Files
Artemis-Eapil/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
T

1414 lines
89 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="agent-stream-container">
<!-- VS Code Style Navigation Header (for responsive small screens) -->
<header class="vscode-nav-header">
<div class="vscode-tab-container">
<button
#taskQueueOrigin="cdkOverlayOrigin"
cdkOverlayOrigin
class="vscode-tab-btn"
[class.active]="isTaskDropdownOpen()"
(click)="toggleTaskDropdown($event)"
title="任务队列"
aria-label="任务队列"
>
<span class="material-symbols-outlined tab-icon">list_alt</span>
<span class="tab-label">任务队列</span>
</button>
<button
#notesOrigin="cdkOverlayOrigin"
cdkOverlayOrigin
class="vscode-tab-btn"
[class.active]="isNotesDropdownOpen()"
(click)="toggleNotesDropdown($event)"
title="笔记与计划"
aria-label="笔记与计划"
>
<span class="material-symbols-outlined tab-icon">description</span>
<span class="tab-label">笔记与计划</span>
</button>
<button #automationOrigin="cdkOverlayOrigin" cdkOverlayOrigin class="vscode-tab-btn" [class.active]="isAutomationOpen()" (click)="isAutomationOpen.set(!isAutomationOpen())" title="报告与批量" aria-label="报告与批量"><span class="material-symbols-outlined tab-icon">assignment</span><span>报告与批量</span></button>
</div>
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="automationOrigin" [cdkConnectedOverlayOpen]="isAutomationOpen()" [cdkConnectedOverlayHasBackdrop]="true" cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop" (backdropClick)="isAutomationOpen.set(false)" [cdkConnectedOverlayPositions]="dropdownPositions"><div class="vscode-dropdown-menu" style="width:min(560px,calc(100vw - 24px));max-height:85vh;overflow:auto;padding:20px"><div class="popover-title-header" style="justify-content:space-between;margin-bottom:6px"><span>报告与自动化</span><button type="button" (click)="isAutomationOpen.set(false)" aria-label="关闭报告与自动化" style="border:0;background:transparent;color:#64748b;cursor:pointer;padding:4px"><span class="material-symbols-outlined">close</span></button></div><app-automation-panel /></div></ng-template>
<!-- Dropdown Popover Panel for Task Queue -->
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="taskQueueOrigin"
[cdkConnectedOverlayOpen]="isTaskDropdownOpen()"
[cdkConnectedOverlayHasBackdrop]="true"
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
(backdropClick)="isTaskDropdownOpen.set(false)"
[cdkConnectedOverlayPositions]="dropdownPositions"
>
<div class="vscode-dropdown-menu task-dropdown">
<div class="popover-title-header">
<span class="material-symbols-outlined">list_alt</span>
<span>任务队列</span>
</div>
<div class="dropdown-section-label">
<span class="material-symbols-outlined icon-bolt">bolt</span>
<span>当前队列 ({{ activeQueue().length }})</span>
</div>
@if (activeQueue().length === 0) {
<div class="dropdown-empty-state">
没有正在执行或排队的任务
</div>
}
@for (session of activeQueue(); track trackSession($index, session)) {
<div
class="dropdown-item-card"
[class.selected]="agentService.currentSessionId() === session.session_id"
[class.running]="getTaskStatus(session) === 'running'"
(click)="selectTask(session.session_id, $event)"
>
<div class="item-header">
<span class="task-badge" [ngClass]="getTaskStatus(session)">
@if (getTaskStatus(session) === 'running') {
<span class="status-dot"></span>
}
{{ getTaskStatus(session) | uiLabel }}
</span>
<div class="item-header-actions">
@if (getDeviceSerial(session)) {
<span class="item-device" [title]="'Device: ' + getDeviceSerial(session)">
<span class="material-symbols-outlined device-icon">phone_android</span>
<span class="device-name">{{ getDeviceSerial(session) }}</span>
</span>
}
@if (getDeviceSerial(session)) {
<span class="item-time-divider">·</span>
}
<span class="item-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
@if (getTaskStatus(session) === 'running' || getTaskStatus(session) === 'paused') {
<button
type="button"
class="btn-stop-card"
(click)="stopTask(session.session_id, $event)"
title="停止此任务"
aria-label="停止此任务"
>
<span class="material-symbols-outlined">stop_circle</span>
</button>
}
@if (getTaskStatus(session) !== 'running' && getTaskStatus(session) !== 'paused') {
<button
type="button"
class="btn-delete-card"
(click)="deleteTask(session.session_id, $event)"
title="删除此任务"
aria-label="删除此任务"
>
<span class="material-symbols-outlined">delete</span>
</button>
}
</div>
</div>
<div class="item-goal" title="{{ session.initial_goal }}">
{{ session.initial_goal || '未命名任务' }}
</div>
<div class="item-footer">
<app-task-id-copy [taskId]="session.session_id" />
<span class="item-date">{{ session.start_time * 1000 | date:'MM/dd/yy' }}</span>
</div>
</div>
}
<app-task-history />
</div>
</ng-template>
<!-- Dropdown Popover Panel for Notes & Plans -->
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="notesOrigin"
[cdkConnectedOverlayOpen]="isNotesDropdownOpen()"
[cdkConnectedOverlayHasBackdrop]="true"
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
(backdropClick)="isNotesDropdownOpen.set(false)"
[cdkConnectedOverlayPositions]="dropdownPositions"
>
<div class="vscode-dropdown-menu notes-dropdown">
<div class="popover-title-header">
<span class="material-symbols-outlined">description</span>
<span>笔记与计划</span>
</div>
<!-- Notes Selector / Pills (Shown if there are note files) -->
@if (noteKeys().length > 1) {
<div class="notes-selector-container">
<div class="notes-selector-list">
@for (key of noteKeys(); track trackNoteKey($index, key)) {
<button
class="note-selector-pill"
[class.active]="agentService.selectedNoteKey() === key"
(click)="selectNote(key, $event)"
>
<span class="material-symbols-outlined pill-icon">draft</span>
{{ key }}
</button>
}
</div>
</div>
}
@if (noteKeys().length === 0) {
<div class="dropdown-empty-state">
<span class="material-symbols-outlined empty-icon">description</span>
<span>此会话暂无笔记。</span>
</div>
}
<!-- Rich Notes Content Viewer -->
@if (noteKeys().length > 0) {
<div class="dropdown-notes-content">
@if (parsedNote().title) {
<h3 class="note-title">{{ parsedNote().title }}</h3>
}
<!-- Milestones list -->
@if (parsedNote().milestones.length > 0) {
<div class="milestones-container">
@for (milestone of parsedNote().milestones; track trackMilestone($index, milestone)) {
<div
class="milestone-block"
[ngClass]="milestone.type"
>
<div class="milestone-title-row">
@switch (milestone.type) {
@case ('checked') {
<span class="material-symbols-outlined milestone-status-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined milestone-status-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined milestone-status-icon unchecked">check_box_outline_blank</span>
}
}
<div class="milestone-text">
@for (seg of milestone.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
<!-- Sub-steps list -->
@if (milestone.subSteps.length > 0) {
<div class="milestone-substeps">
@for (step of milestone.subSteps; track trackMarkdownLine($index, step)) {
<div
class="substep-row"
[ngClass]="step.type"
[style.padding-left.px]="(step.indent > 2 ? (step.indent - 2) * 12 : 0)"
>
@switch (step.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (step.type !== 'empty') {
<span class="line-text">
@for (seg of step.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
</div>
}
<!-- Verification result -->
@if (milestone.checks.length > 0) {
<div class="milestone-checks-container">
@for (check of milestone.checks; track trackMarkdownLine($index, check)) {
<div
class="verification-card"
[ngClass]="check.type"
[style.margin-left.px]="(check.indent > 2 ? (check.indent - 2) * 12 : 0)"
>
<div class="card-header">
<span class="card-tag" [class.assert]="check.checkKind === 'assert'">{{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of check.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
}
</div>
}
</div>
}
</div>
}
<!-- Other Markdown Lines Container -->
@if (parsedNote().otherLines.length > 0) {
<div class="other-lines-container">
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
@if (line.type === 'verify' || line.type === 'finding') {
<div class="verification-card standalone" [ngClass]="line.type" [style.margin-left.px]="line.indent * 12">
<div class="card-header">
<span class="card-tag" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
} @else {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
}
</div>
}
</div>
}
</div>
</ng-template>
</header>
<!-- Floating Top-Right Badges & Toolbar (Pinned overlay, hidden in empty/initial state) -->
@if (consolidatedBlocks().length > 0 || showPlanningLoader() || agentService.currentSession()) {
<div class="stream-floating-toolbar">
<!-- Active Architecture Chip: click opens the run info popover -->
@if (agentService.viewedModel(); as model) {
<button
type="button"
#runInfoOrigin="cdkOverlayOrigin"
cdkOverlayOrigin
class="model-info-chip"
[ngClass]="getModelClass(model.name)"
[class.active]="isRunInfoOpen()"
[title]="getArchitectureTooltip(model)"
aria-haspopup="dialog"
[attr.aria-expanded]="isRunInfoOpen()"
(click)="toggleRunInfo($event)"
>
<span class="material-symbols-outlined model-icon">{{ getModelIcon(model.name) }}</span>
<span class="model-label">执行模式:</span>
<span class="model-name">{{ getModelDisplayName(model.name) }}</span>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="runInfoOrigin"
[cdkConnectedOverlayOpen]="isRunInfoOpen()"
[cdkConnectedOverlayHasBackdrop]="true"
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
(backdropClick)="isRunInfoOpen.set(false)"
[cdkConnectedOverlayPositions]="dropdownPositions"
>
<div class="vscode-dropdown-menu run-info-dropdown" role="dialog" aria-label="运行信息">
<div class="run-info-header">
<span class="material-symbols-outlined" [class.is-pro]="runInfoIsPro()">{{ getModelIcon(model.name) }}</span>
<span class="run-info-title">{{ getModelDisplayName(model.name) }}</span>
@if (model.id) {
<span class="run-info-model" [title]="model.id">{{ model.id }}</span>
}
</div>
<div class="run-info-row">
<span class="run-info-label">已用时间</span>
<span class="run-info-value">{{ runInfoElapsed() }}</span>
</div>
<div class="run-info-row">
<span class="run-info-label">Token 用量</span>
@if (runUsage(); as usage) {
<span class="run-info-value">
{{ formatRunTokens(usage.total_tokens) }}
<span class="run-info-sub">{{ formatRunTokens(usage.prompt_tokens) }} 输入 · {{ formatRunTokens(usage.completion_tokens) }} 输出</span>
</span>
} @else {
<span class="run-info-value is-muted">—</span>
}
</div>
<div class="run-info-row is-stacked">
<div class="run-info-line">
<span class="run-info-label">上下文</span>
@if (runInfoContextPercent() !== null) {
<span class="run-info-value">
{{ runInfoContextPercent() }}%
<span class="run-info-sub-inline">/ {{ formatRunTokens(runUsage()?.operator_context_window_tokens) }}</span>
</span>
} @else {
<span class="run-info-value is-muted">—</span>
}
</div>
<div
class="run-info-meter"
[class.is-high]="(runInfoContextPercent() ?? 0) >= 70"
[class.is-critical]="(runInfoContextPercent() ?? 0) >= 90"
>
<div class="run-info-meter-fill" [style.width.%]="runInfoContextPercent() ?? 0"></div>
</div>
</div>
@if (runInfoIsPro()) {
<div class="run-info-row">
<span class="run-info-label">结果检查</span>
<span class="run-info-value">{{ runTuningLabel('verify', runInfoTuning()?.verification_level) }}</span>
</div>
<div class="run-info-row">
<span class="run-info-label">屏幕识别</span>
<span class="run-info-value">{{ runTuningLabel('explore', runInfoTuning()?.explorer_mode) }}</span>
</div>
} @else {
<div class="run-info-note">Flash 无需额外设置。</div>
}
</div>
</ng-template>
}
<!-- Screen Recording Action Button (Interactive Button) -->
<button
class="btn-screen-recording"
[class.active]="agentService.isVideoWindowOpen()"
[class.recording]="agentService.isCurrentSessionRunning() && agentService.currentSession()?.recording_status === 'recording'"
[class.processing]="!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() === 'processing'"
[class.has-video]="!agentService.isCurrentSessionRunning() && !!agentService.currentSessionVideoUrl()"
[class.has-steps]="!agentService.isCurrentSessionRunning() && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()"
[class.failed]="!agentService.isCurrentSessionRunning() && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() === 'failed'"
(click)="agentService.toggleVideoPlayer()"
[title]="getScreenRecordingButtonTitle()"
>
@if (agentService.isCurrentSessionRunning()) {
<span class="material-symbols-outlined video-icon">
{{ agentService.currentSession()?.recording_status === 'recording' ? 'videocam' : 'progress_activity' }}
</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() === 'processing') {
<span class="material-symbols-outlined video-icon spin-animation">
progress_activity
</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && agentService.currentSessionVideoUrl()) {
<span class="material-symbols-outlined video-icon play-icon">
smart_display
</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
<span class="material-symbols-outlined video-icon steps-icon">
slideshow
</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() === 'failed') {
<span class="material-symbols-outlined video-icon error-icon">
videocam_off
</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() !== 'failed') {
<span class="material-symbols-outlined video-icon">
videocam
</span>
}
@if (agentService.isCurrentSessionRunning()) {
<span class="btn-text">{{ agentService.currentSession()?.recording_status === 'recording' ? '正在录制…' : '任务执行中…' }}</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() === 'processing') {
<span class="btn-text">正在准备…</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && agentService.currentSessionVideoUrl()) {
<span class="btn-text">播放录像</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
<span class="btn-text">步骤回放</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() === 'failed') {
<span class="btn-text">录屏失败</span>
}
@if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() !== 'failed') {
<span class="btn-text">屏幕录像</span>
}
@if (agentService.isCurrentSessionRunning()) {
<span class="video-pulse-dot red"></span>
}
@if (!agentService.isCurrentSessionRunning() && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
<span class="steps-count-badge">
{{ agentService.currentSessionStepFrames().length }}
</span>
}
</button>
<ng-content select="[task-guidance]"></ng-content>
</div>
}
<!-- Event Logs Section -->
<main class="stream-logs-content">
@if (agentService.isSessionContentLoading()) {
<div class="session-loading-state" aria-live="polite">
<span class="material-symbols-outlined spin-animation">progress_activity</span>
<span>正在加载执行历史…</span>
</div>
}
@if (!agentService.isSessionContentLoading() && consolidatedBlocks().length === 0 && !showPlanningLoader() && startupWorkItems().length === 0) {
<div class="empty-state">
<div class="empty-icon"><span class="material-symbols-outlined">sensors</span></div>
<h3>尚未选择执行记录</h3>
<p>请从右侧任务列表选择会话,查看实时执行记录。</p>
</div>
}
<!-- The phase-based session logs list -->
@if (!agentService.isSessionContentLoading() && (phases().length > 0 || showPlanningLoader() || startupWorkItems().length > 0)) {
<div class="logs-list-container">
@if (startupWorkItems().length > 0) {
<div class="phase-container startup-phase" aria-live="polite">
<div class="phase-header first-phase">
<span class="phase-worked-time">准备耗时 {{ startupPreparationDuration() }} 秒</span>
</div>
<div class="phase-body" aria-label="任务启动进度">
<div class="block-item type-stream">
<div class="step-body">
<div class="step-actions-container">
<div class="llm-retry-notes">
@for (item of startupWorkItems(); track item) {
<div class="llm-retry-note">
<span class="stream-status-dot" [class.streaming]="item.isActive"></span>
<div class="llm-retry-note-text">
<span class="llm-retry-status-line">
{{ item.message }} · {{ item.isActive ? 'Waiting ' : 'Waited ' }}{{ item.elapsed }}
</span>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
}
@for (phase of phases(); track trackPhase($index, phase); let first = $first) {
<div class="phase-container">
@if (hasVisibleBlocks(phase)) {
<div
class="phase-header"
[class.first-phase]="first && startupWorkItems().length === 0"
>
<span class="phase-worked-time">{{ isCheckerPhase(phase) ? 'Checked' : 'Worked' }} for {{ phase.durationSeconds }}s</span>
@if (phase.tokens) {
<span class="phase-token-usage" [title]="getTokenTooltip(phase)">
<span class="token-dot">·</span>
<span class="token-text">{{ formatTokenCount(phase.tokens) }}</span>
</span>
}
</div>
}
@if (hasVisibleBlocks(phase)) {
<div class="phase-body">
@for (block of phase.blocks; track trackBlock($index, block)) {
@if (hasVisibleContent(block)) {
<div
class="block-item"
[ngClass]="['type-' + block.type]"
>
<!-- Unified Step / Stream Body -->
<div class="step-body">
@if (block.data?.extra_metadata?.injected_instruction; as guidance) {
<div class="guidance-step-note">
<div class="guidance-step-title"><span class="material-symbols-outlined">forum</span>用户指导 · 已进入第 {{block.data.step_number}} 步</div>
<p>{{ guidance }}</p>
<small>{{ hasRecordedDeviceAction(block.data.action_taken) ? '本步设备动作见下方;是否完成目标需以实际屏幕为准。' : '本步没有记录设备动作。' }}</small>
</div>
}
<!-- Check block (verifier lane): header, then the declared check items
and how each is judged; the chronological stream follows, and the
result rows close the block (see check-result below). -->
@if (isCheckerBlock(block)) {
<div class="check-lane" [ngClass]="'tone-' + getCheckerTone(block)">
<div class="check-header">
<span class="stream-status-dot" [class.streaming]="isBlockRunning(block)"></span>
<span class="check-title">{{ getCheckerPhaseLabel(block) }}</span>
@if (block.data.phase !== 'outcome' && block.data.subgoal_text) {
<span class="check-subject" [title]="block.data.subgoal_text">{{ block.data.subgoal_text }}</span>
}
<span class="check-status">{{ getCheckerStatusLabel(block) }}</span>
</div>
@if (!isCheckerMuted(block) && getCheckerItems(block).length > 0) {
<ol class="check-items">
@for (item of getCheckerItems(block); track $index) {
<li class="check-item">
<span class="check-index">{{ $index + 1 }}</span>
<span class="check-text">{{ item.text }}</span>
<span class="check-method">{{ getCheckMethodLabel(item, block) }}</span>
</li>
}
</ol>
}
</div>
}
<!-- Text, tool calls, and actions share one chronological timeline. -->
@if (getSortedStepEvents(block.data).length > 0) {
<div class="step-actions-container">
@for (item of getSortedStepEvents(block.data); track trackStepEvent(itemIdx, item); let itemIdx = $index) {
<!-- Check block: one Thought / Work entry per streamed turn, in time order with its tool calls -->
@if (isCheckerBlock(block) && item.type === 'thinking') {
<div
class="thinking-stream-details native-thinking-details"
[class.is-running]="isCheckerSegmentRunning(block, itemIdx) && !isCheckerSegmentResetNotice(item)"
[class.is-resetting]="isCheckerSegmentResetNotice(item)"
[class.collapsed]="isCheckerSegmentCollapsed(block, itemIdx)">
<div class="details-summary" (click)="toggleCheckerSegment(block, itemIdx)">
<span class="stream-status-dot" [class.streaming]="isCheckerSegmentRunning(block, itemIdx) && !isCheckerSegmentResetNotice(item)" [class.resetting]="isCheckerSegmentResetNotice(item)"></span>
<span class="header-label">{{ isCheckerSegmentResetNotice(item) ? getCheckerSegmentResetMessage(item) : '思考过程' }}</span>
</div>
@if (item.data.text && item.data.text.trim()) {
<div class="details-content-wrapper">
<div class="details-content stream-box">
<div class="stream-text" [innerHTML]="renderMarkdown(item.data.text, block.id + '-seg-' + itemIdx)"></div>
</div>
</div>
}
</div>
}
@if (isCheckerBlock(block) && item.type === 'text' && isCheckerTextVisible(item)) {
@if (item.data.isReset) {
<div class="llm-retry-notes">
<div class="llm-retry-note">
<span class="stream-status-dot" [class.streaming]="isCheckerSegmentRunning(block, itemIdx)"></span>
<div class="llm-retry-note-text">
<span class="llm-retry-status-line">{{ formatResetMessage(getCheckerSegmentResetMessage(item), isCheckerSegmentRunning(block, itemIdx)) }}</span>
</div>
</div>
</div>
}
@if (item.data.text && item.data.text.trim()) {
<div
class="step-raw-thinking-message"
[class.is-running]="isCheckerSegmentRunning(block, itemIdx)">
<div class="report-header">
<span class="stream-status-dot" [class.streaming]="isCheckerSegmentRunning(block, itemIdx)"></span>
<span class="header-label">执行过程</span>
</div>
<div class="report-content stream-box">
<div class="stream-text" [innerHTML]="renderMarkdown(item.data.text, block.id + '-seg-' + itemIdx)"></div>
</div>
</div>
}
}
<!-- Native Thinking (Thought) -->
@if (!isCheckerBlock(block) && item.type === 'thinking') {
@if (getBlockResets(block, true).length > 0) {
<div class="llm-retry-notes">
@for (reset of getBlockResets(block, true); track reset.id || $index) {
<div class="llm-retry-note">
<span class="stream-status-dot" [class.streaming]="reset.isWaiting"></span>
<div class="llm-retry-note-text">
<span class="llm-retry-status-line">{{ formatResetMessage(reset.message, reset.isWaiting) }}</span>
</div>
</div>
}
</div>
}
@if (getStreamDisplayText(block.id + '-native', item.data.text) || (isBlockRunning(block) && !isStreamRewinding(block.id + '-native') && !hasWaitingReset(block, true))) {
<div
class="thinking-stream-details native-thinking-details"
[class.is-running]="isBlockRunning(block)"
[class.collapsed]="collapsedStreams().has(block.data.execution_id || block.data.step_id || block.id)">
<div class="details-summary" (click)="toggleCollapse(block.data.execution_id || block.data.step_id || block.id)">
<span class="stream-status-dot" [class.streaming]="isBlockRunning(block)"></span>
<span class="header-label">思考过程</span>
</div>
@if (getStreamDisplayText(block.id + '-native', item.data.text)) {
<div class="details-content-wrapper">
<div class="details-content stream-box">
<div class="stream-text" [innerHTML]="renderMarkdown(getStreamDisplayText(block.id + '-native', item.data.text), block.id + '-native')"></div>
</div>
</div>
}
</div>
}
}
<!-- Raw Thinking / Plan / Message (Work) -->
@if (!isCheckerBlock(block) && item.type === 'text') {
@if (getBlockResets(block, false).length > 0) {
<div class="llm-retry-notes">
@for (reset of getBlockResets(block, false); track reset.id || $index) {
<div class="llm-retry-note">
<span class="stream-status-dot" [class.streaming]="reset.isWaiting"></span>
<div class="llm-retry-note-text">
<span class="llm-retry-status-line">{{ formatResetMessage(reset.message, reset.isWaiting) }}</span>
</div>
</div>
}
</div>
}
@if (getStreamDisplayText(block.id, item.data.text) || (isBlockRunning(block) && !isStreamRewinding(block.id) && !hasWaitingReset(block, false))) {
<div
class="step-raw-thinking-message"
[class.is-running]="isBlockRunning(block)">
<div class="report-header">
<span class="stream-status-dot" [class.streaming]="isBlockRunning(block)"></span>
<span class="header-label">执行过程</span>
</div>
@if (getStreamDisplayText(block.id, item.data.text)) {
<div class="report-content stream-box">
<div class="stream-text" [innerHTML]="renderMarkdown(getStreamDisplayText(block.id, item.data.text), block.id)"></div>
</div>
}
</div>
}
}
<!-- Task Submission / Report Card (when item is report_task_status action or tool) -->
@if (isReportStatusAction(item.data)) {
<div class="step-android-action-card task-report-card"
[class.is-completed]="getReportStatusValue(item.data) === 'completed' || getReportStatusValue(item.data) === 'success'"
[class.is-failed]="getReportStatusValue(item.data) === 'failed'"
[class.is-expanded]="isActionExpanded('action-' + block.id + '-' + itemIdx)"
>
<div class="action-card-header" (click)="toggleActionExpanded('action-' + block.id + '-' + itemIdx, $event)">
<span class="material-symbols-outlined action-icon report-icon" [class.is-failed]="getReportStatusValue(item.data) === 'failed'">
{{ getReportStatusValue(item.data) === 'failed' ? 'cancel' : 'task_alt' }}
</span>
<span class="action-title">
{{ getReportStatusValue(item.data) === 'failed' ? '任务执行失败' : '任务已完成' }}
</span>
<span class="action-status-badge" [class.is-failed]="getReportStatusValue(item.data) === 'failed'" [class.is-completed]="getReportStatusValue(item.data) !== 'failed'">
<span class="material-symbols-outlined status-icon">
{{ getReportStatusValue(item.data) === 'failed' ? 'error' : 'check_circle' }}
</span>
{{ getReportStatusValue(item.data) === 'failed' ? '失败' : '已完成' }}
</span>
<span class="action-expand-icon material-symbols-outlined">
{{ isActionExpanded('action-' + block.id + '-' + itemIdx) ? 'expand_less' : 'expand_more' }}
</span>
</div>
<!-- Compact preview body (when collapsed, matches standard action card) -->
@if (!isActionExpanded('action-' + block.id + '-' + itemIdx) && getReportStatusExplanation(item.data)) {
<div class="action-card-body">
<div class="action-detail-row">
<span class="detail-label">摘要:</span>
<span class="detail-value">"{{ getReportStatusExplanation(item.data) }}"</span>
</div>
</div>
}
<!-- Detailed expanded view (when clicked, matches standard action card structure) -->
@if (isActionExpanded('action-' + block.id + '-' + itemIdx)) {
<div class="action-card-expanded-details" (click)="$event.stopPropagation()">
<div class="expanded-grid">
@if (getReportStatusExplanation(item.data); as expText) {
<div class="grid-item full-width">
<span class="grid-label">报告与发现</span>
<div class="grid-value report-markdown-body" [innerHTML]="renderMarkdown(expText, 'report-' + block.id + '-' + itemIdx)"></div>
</div>
}
</div>
<!-- Final Verified Screenshot -->
@if (getStepPreImageUrl(block.data, item.data) || getStepPostImageUrl(block.data, item.data)) {
<div class="expanded-screenshots-container">
@if (getStepPreImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">最终验证状态</span>
<div class="image-wrapper" (click)="openImageModal(getStepPreImageUrl(block.data, item.data)!, item.data, $event)">
<div class="img-relative-box">
<img [src]="getStepPreImageUrl(block.data, item.data)" alt="最终屏幕截图" />
</div>
</div>
</div>
}
@if (getStepPostImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">操作后验证</span>
<div class="image-wrapper" (click)="openImageModal(getStepPostImageUrl(block.data, item.data)!, null, $event)">
<div class="img-relative-box">
<img [src]="getStepPostImageUrl(block.data, item.data)" alt="操作后截图" />
</div>
</div>
</div>
}
</div>
}
</div>
}
</div>
}
<!-- Android Action Card (when item is an action) -->
@if (item.type === 'action' && !isReportStatusAction(item.data) && (isAndroidAction(item.data) || item.data?.type === 'action')) {
<div class="step-android-action-card"
[class.is-running]="isItemRunning(item, block)"
[class.is-failed]="isActionFailed(item.data, block.data)"
[class.is-expanded]="isActionExpanded('action-' + block.id + '-' + itemIdx)"
>
<div class="action-card-header" (click)="toggleActionExpanded('action-' + block.id + '-' + itemIdx, $event)">
<span class="material-symbols-outlined action-icon" [class.is-failed]="isActionFailed(item.data, block.data)">{{ getActionIcon(item.data) }}</span>
<span class="action-title">{{ getActionTitle(item.data) }}</span>
@if (isActionFailed(item.data, block.data)) {
<span class="action-status-badge is-failed">
<span class="material-symbols-outlined status-icon">error</span>
失败
</span>
}
<span class="action-expand-icon material-symbols-outlined">
{{ isActionExpanded('action-' + block.id + '-' + itemIdx) ? 'expand_less' : 'expand_more' }}
</span>
</div>
<!-- Compact preview body (when collapsed) -->
@if (!isActionExpanded('action-' + block.id + '-' + itemIdx) && (getActionTargetText(item.data) || getActionInputText(item.data) || getActionCoords(item.data))) {
<div class="action-card-body">
@if (getActionTargetText(item.data)) {
<div class="action-detail-row">
<span class="detail-label">目标:</span>
<span class="detail-value">"{{ getActionTargetText(item.data) }}"</span>
</div>
}
@if (getActionInputText(item.data)) {
<div class="action-detail-row">
<span class="detail-label">{{ getActionInputLabel(item.data) }}:</span>
<span class="detail-value">"{{ getActionInputText(item.data) }}"</span>
</div>
}
@if (getActionCoords(item.data)) {
<div class="action-detail-row">
<span class="detail-label">坐标:</span>
<span class="detail-value coordinate-badge">{{ getActionCoords(item.data) }}</span>
</div>
}
</div>
}
<!-- Detailed expanded view (when clicked) -->
@if (isActionExpanded('action-' + block.id + '-' + itemIdx)) {
<div class="action-card-expanded-details" (click)="$event.stopPropagation()">
<div class="expanded-grid">
@if (getActionTargetText(item.data)) {
<div class="grid-item">
<span class="grid-label">目标</span>
<span class="grid-value">"{{ getActionTargetText(item.data) }}"</span>
</div>
}
@if (getActionInputText(item.data)) {
<div class="grid-item">
<span class="grid-label">{{ getActionInputLabel(item.data) }}</span>
<span class="grid-value">"{{ getActionInputText(item.data) }}"</span>
</div>
}
@if (getActionCoords(item.data)) {
<div class="grid-item">
<span class="grid-label">坐标</span>
<span class="grid-value code-pill">{{ getActionCoords(item.data) }}</span>
</div>
}
@if (getActionBounds(item.data)) {
<div class="grid-item">
<span class="grid-label">目标边界</span>
<span class="grid-value code-pill">[{{ getActionBounds(item.data) }}]</span>
</div>
}
@if (getActionClass(item.data)) {
<div class="grid-item">
<span class="grid-label">目标类型</span>
<span class="grid-value code-pill">{{ getActionClass(item.data) }}</span>
</div>
}
@for (param of getActionExtraParams(item.data); track trackParam($index, param)) {
<div class="grid-item">
<span class="grid-label">{{ param.key }}</span>
<span class="grid-value code-pill">{{ param.value }}</span>
</div>
}
<!-- Fallback item if all detail fields are empty -->
@if (!getActionTargetText(item.data) && !getActionInputText(item.data) && !getActionCoords(item.data) && !getActionBounds(item.data) && !getActionClass(item.data) && getActionExtraParams(item.data).length === 0) {
<div class="grid-item">
<span class="grid-label">操作</span>
<span class="grid-value">{{ getActionTitle(item.data) }}</span>
</div>
}
</div>
<!-- Action Screenshots (Pre & Post Action) -->
@if (getStepPreImageUrl(block.data, item.data) || getStepPostImageUrl(block.data, item.data)) {
<div class="expanded-screenshots-container">
@if (getStepPreImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">操作前</span>
<div class="image-wrapper" (click)="openImageModal(getStepPreImageUrl(block.data, item.data)!, item.data, $event)">
<div class="img-relative-box">
<img #preImg [src]="getStepPreImageUrl(block.data, item.data)" (load)="onImageLoad(preImg, preOverlay, item.data)" alt="操作前截图" />
<div #preOverlay class="overlay-layer"></div>
</div>
</div>
</div>
}
@if (getStepPostImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">操作后</span>
<div class="image-wrapper" (click)="openImageModal(getStepPostImageUrl(block.data, item.data)!, null, $event)">
<div class="img-relative-box">
<img [src]="getStepPostImageUrl(block.data, item.data)" alt="操作后截图" />
</div>
</div>
</div>
}
</div>
}
@if (isActionFailed(item.data, block.data)) {
<div class="expanded-error-banner">
<span class="material-symbols-outlined icon">error</span>
<span class="error-text">{{ getActionErrorMessage(item.data, block.data) }}</span>
</div>
}
</div>
}
</div>
}
<!-- Generic Tools & LLM Calls (when item is a tool) -->
@if (item.type === 'tool') {
<!-- UI-hierarchy source changed mid-run (helper <-> UIAutomator2). -->
@if (isBackendSwitchNote(item.data)) {
<div class="llm-retry-notes">
<div class="llm-retry-note">
<span class="stream-status-dot"></span>
<div class="llm-retry-note-text">
<span class="llm-retry-status-line">{{ getBackendSwitchMessage(item.data) }}</span>
</div>
</div>
</div>
}
<!-- Only SDK-observed Gemini retries are shown as quiet stream notes. -->
@if (isLLMRetry(item.data)) {
<div class="llm-retry-notes">
@for (retry of getLLMRetryEntries(item.data); track retry; let retryIndex = $index) {
<div class="llm-retry-note">
<span class="stream-status-dot"
[class.streaming]="retryIndex === getLLMRetryEntries(item.data).length - 1 && isLLMRetryEntryWaiting(retry)"></span>
<div class="llm-retry-note-text">
@if (isLLMRetryEntryWaiting(retry)) {
<span class="llm-retry-status-line">
模型暂时不可用
@if (getLLMRetryEntryDelay(retry); as retryDelay) {
· Retrying in {{ retryDelay }}…
}
</span>
<span class="llm-retry-guidance">
频繁重试可能是接口限流导致,可提高调用额度或降低并发数。
</span>
}
@if (!isLLMRetryEntryWaiting(retry)) {
<span class="llm-retry-status-line">
Retrying LLM request · Waited {{ getLLMRetryEntryWaited(retry) }}
</span>
}
</div>
</div>
}
</div>
}
<!-- Failed LLM Calls (Permanently maintain Action Card format in red) -->
@if (isDisplayableLLMFailure(item.data)) {
<div class="llm-failure-stack">
<div class="step-android-action-card tool-action-card is-failed"
[class.has-details]="hasLLMFailureRetryEntries(item.data)"
[class.is-expanded]="isActionExpanded('llm-failure-' + block.id + '-' + itemIdx)"
(click)="hasLLMFailureRetryEntries(item.data) && toggleActionExpanded('llm-failure-' + block.id + '-' + itemIdx, $event)">
<div class="action-card-header llm-failure-header">
<span class="material-symbols-outlined action-icon is-failed">error</span>
<span class="action-title">模型请求失败</span>
@if (isTerminalLLMFailure(item.data)) {
<span class="action-status-badge is-paused">
<span class="material-symbols-outlined status-icon">pause_circle</span>
已暂停
</span>
} @else {
<span class="action-status-badge is-failed">
<span class="material-symbols-outlined status-icon">warning</span>
失败
</span>
}
@if (hasLLMFailureRetryEntries(item.data)) {
<span class="action-expand-icon material-symbols-outlined">
{{ isActionExpanded('llm-failure-' + block.id + '-' + itemIdx) ? 'expand_less' : 'expand_more' }}
</span>
}
</div>
<div class="action-card-body">
<div class="action-detail-row">
<span class="detail-label">发生了什么:</span>
<span class="detail-value">自动重试后仍无法完成模型请求。</span>
</div>
<div class="action-detail-row">
<span class="detail-label">失败原因:</span>
<span class="detail-value">{{ getLLMErrorText(item.data) }}</span>
</div>
@if (getLLMFailureWaited(item.data); as waited) {
<div class="action-detail-row">
<span class="detail-label">已等待:</span>
<span class="detail-value">{{ waited }}</span>
</div>
}
@if (isActionExpanded('llm-failure-' + block.id + '-' + itemIdx)) {
<div class="failure-retry-details">
<div class="failure-retry-title">重试详情</div>
@for (retry of getLLMFailureRetryEntries(item.data); track retry; let retryIndex = $index) {
<div class="failure-retry-row">
<span>Retry {{ retryIndex + 1 }}</span>
<span>
Planned {{ getLLMRetryEntryDelay(retry) || '0s' }}
· Waited {{ getLLMRetryEntryWaited(retry) }}
</span>
</div>
}
</div>
}
</div>
</div>
@if (isTerminalLLMFailure(item.data)) {
<div class="llm-resume-row">
<button
type="button"
class="btn-continue-task"
(click)="resumePausedTask($event)"
title="继续已暂停的任务"
aria-label="继续已暂停的任务"
>
<span class="material-symbols-outlined continue-icon">play_arrow</span>
<span>继续任务</span>
<span class="material-symbols-outlined continue-arrow">arrow_forward</span>
</button>
<span class="llm-resume-hint">任务已暂停 · 当前没有执行中的步骤</span>
</div>
}
</div>
}
<!-- Generic Tool Calls (Text format for note tools, video analyzer, and other sub-agents) -->
@if (shouldShowTool(item.data, block.data) && !isDeviceActionTool(item.data)) {
<div class="step-generic-tool"
[class.is-running]="isToolLineRunning(item, block)"
[class.is-note-tool]="isNoteTool(item.data)"
[class.is-video-tool]="isVideoTool(item.data)"
[class.is-video-attention]="isVideoTool(item.data) && isVideoAnalysisAttention(item.data)"
[attr.title]="getCompressionPhaseTitle(item.data)"
>
<div class="tool-call-label">
<span class="stream-status-dot" [class.streaming]="isToolLineRunning(item, block)"></span>
<div class="tool-call-text">
<span>{{ isVideoTool(item.data) ? getVideoAnalysisLabel(item.data) : getToolDisplayLabel(item.data) }}{{ ((isNoteTool(item.data) && getToolKey(item.data)) || isVideoTool(item.data)) ? ':' : '' }}</span>
<!-- Note Tool Pill -->
@if (isNoteTool(item.data) && getToolKey(item.data); as key) {
<span
class="tool-key-pill"
(click)="onToolKeyClick(key)"
title="点击查看笔记内容"
>
<span class="material-symbols-outlined pill-icon">draft</span>
{{ key }}
</span>
}
<!-- Video Tool Pill -->
@if (isVideoTool(item.data)) {
<span
class="tool-key-pill"
(click)="onVideoToolClick(item.data)"
[title]="'Open recording at ' + getVideoAnalysisRangeLabel(item.data)"
>
<span class="material-symbols-outlined pill-icon">videocam</span>
{{ getVideoAnalysisRangeLabel(item.data) }}
</span>
@if (getVideoAnalysisDetail(item.data); as detail) {
<span class="video-status-detail">· {{ detail }}</span>
}
}
</div>
</div>
</div>
}
<!-- Action Card format (For Device Actions & ADB Commands) -->
@if (shouldShowTool(item.data, block.data) && isDeviceActionTool(item.data)) {
<div class="step-android-action-card tool-action-card"
[class.is-running]="isItemRunning(item, block)"
[class.has-no-details]="isAdbCommandTool(item.data) ? !getAdbCommandLine(item.data) : (!getToolTargetText(item.data) && !getToolInputText(item.data) && !getToolCoords(item.data) && !getToolAnalysisText(item.data) && !getToolGenericDetails(item.data) && getToolExtraParams(item.data).length === 0)"
[class.is-failed]="isToolFailed(item.data)"
[class.is-expanded]="isActionExpanded('tool-' + block.id + '-' + itemIdx)"
>
<div class="action-card-header" (click)="toggleActionExpanded('tool-' + block.id + '-' + itemIdx, $event)">
<span class="material-symbols-outlined action-icon" [class.is-failed]="isToolFailed(item.data)">{{ getToolIcon(item.data) }}</span>
<span class="action-title">{{ getToolTitle(item.data) }}</span>
@if (isToolFailed(item.data)) {
<span class="action-status-badge is-failed">
<span class="material-symbols-outlined status-icon">error</span>
失败
</span>
}
<span class="action-expand-icon material-symbols-outlined">
{{ isActionExpanded('tool-' + block.id + '-' + itemIdx) ? 'expand_less' : 'expand_more' }}
</span>
</div>
<!-- Compact preview body (when collapsed) -->
@if (!isActionExpanded('tool-' + block.id + '-' + itemIdx) && (isAdbCommandTool(item.data) ? getAdbCommandLine(item.data) : (getToolTargetText(item.data) || getToolInputText(item.data) || getToolCoords(item.data) || getToolAnalysisText(item.data) || getToolGenericDetails(item.data)))) {
<div class="action-card-body">
<!-- ADB command details -->
@if (isAdbCommandTool(item.data)) {
@if (getAdbCommandLine(item.data)) {
<div class="action-detail-row">
<span class="detail-label">命令:</span>
<span class="detail-value command-value"><code>{{ getAdbCommandLine(item.data) }}</code></span>
</div>
}
@if (getAdbCwd(item.data)) {
<div class="action-detail-row">
<span class="detail-label">工作目录:</span>
<span class="detail-value">"{{ getAdbCwd(item.data) }}"</span>
</div>
}
@if (getAdbTerminalId(item.data)) {
<div class="action-detail-row">
<span class="detail-label">终端 ID:</span>
<span class="detail-value">"{{ getAdbTerminalId(item.data) }}"</span>
</div>
}
}
<!-- Standard details -->
@if (!isAdbCommandTool(item.data)) {
@if (getToolTargetText(item.data)) {
<div class="action-detail-row">
<span class="detail-label">目标:</span>
<span class="detail-value">"{{ getToolTargetText(item.data) }}"</span>
</div>
}
@if (getToolInputText(item.data)) {
<div class="action-detail-row">
<span class="detail-label">{{ getToolInputLabel(item.data) }}:</span>
<span class="detail-value">"{{ getToolInputText(item.data) }}"</span>
</div>
}
@if (getToolCoords(item.data)) {
<div class="action-detail-row">
<span class="detail-label">坐标:</span>
<span class="detail-value coordinate-badge">{{ getToolCoords(item.data) }}</span>
</div>
}
@if (getToolAnalysisText(item.data)) {
<div class="action-detail-row">
<span class="detail-label">分析:</span>
<span class="detail-value">"{{ getToolAnalysisText(item.data) }}"</span>
</div>
}
@if (getToolGenericDetails(item.data)) {
<div class="action-detail-row">
<span class="detail-label">详情:</span>
<span class="detail-value">"{{ getToolGenericDetails(item.data) }}"</span>
</div>
}
}
</div>
}
<!-- Detailed expanded view (when clicked) -->
@if (isActionExpanded('tool-' + block.id + '-' + itemIdx)) {
<div class="action-card-expanded-details" (click)="$event.stopPropagation()">
<div class="expanded-grid">
@if (isAdbCommandTool(item.data)) {
@if (getAdbCommandLine(item.data)) {
<div class="grid-item full-width">
<span class="grid-label">命令</span>
<span class="grid-value command-value"><code>{{ getAdbCommandLine(item.data) }}</code></span>
</div>
}
@if (getAdbCwd(item.data)) {
<div class="grid-item">
<span class="grid-label">工作目录</span>
<span class="grid-value">"{{ getAdbCwd(item.data) }}"</span>
</div>
}
@if (getAdbTerminalId(item.data)) {
<div class="grid-item">
<span class="grid-label">终端 ID</span>
<span class="grid-value code-pill">{{ getAdbTerminalId(item.data) }}</span>
</div>
}
}
@if (!isAdbCommandTool(item.data)) {
@if (getToolTargetText(item.data)) {
<div class="grid-item">
<span class="grid-label">目标</span>
<span class="grid-value">"{{ getToolTargetText(item.data) }}"</span>
</div>
}
@if (getToolInputText(item.data)) {
<div class="grid-item">
<span class="grid-label">{{ getToolInputLabel(item.data) }}</span>
<span class="grid-value">"{{ getToolInputText(item.data) }}"</span>
</div>
}
@if (getToolCoords(item.data)) {
<div class="grid-item">
<span class="grid-label">坐标</span>
<span class="grid-value code-pill">{{ getToolCoords(item.data) }}</span>
</div>
}
@if (getToolAnalysisText(item.data)) {
<div class="grid-item">
<span class="grid-label">分析</span>
<span class="grid-value">"{{ getToolAnalysisText(item.data) }}"</span>
</div>
}
@if (getToolGenericDetails(item.data)) {
<div class="grid-item">
<span class="grid-label">详情</span>
<span class="grid-value">"{{ getToolGenericDetails(item.data) }}"</span>
</div>
}
@for (param of getToolExtraParams(item.data); track trackParam($index, param)) {
<div class="grid-item">
<span class="grid-label">{{ param.key }}</span>
<span class="grid-value code-pill">{{ param.value }}</span>
</div>
}
}
</div>
<!-- Action Screenshots (Pre & Post Action) -->
@if (getStepPreImageUrl(block.data, item.data) || getStepPostImageUrl(block.data, item.data)) {
<div class="expanded-screenshots-container">
@if (getStepPreImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">操作前</span>
<div class="image-wrapper" (click)="openImageModal(getStepPreImageUrl(block.data, item.data)!, item.data, $event)">
<div class="img-relative-box">
<img #preImgTool [src]="getStepPreImageUrl(block.data, item.data)" (load)="onImageLoad(preImgTool, preOverlayTool, item.data)" alt="操作前截图" />
<div #preOverlayTool class="overlay-layer"></div>
</div>
</div>
</div>
}
@if (getStepPostImageUrl(block.data, item.data)) {
<div class="screenshot-column">
<span class="screenshot-label">操作后</span>
<div class="image-wrapper" (click)="openImageModal(getStepPostImageUrl(block.data, item.data)!, null, $event)">
<div class="img-relative-box">
<img [src]="getStepPostImageUrl(block.data, item.data)" alt="操作后截图" />
</div>
</div>
</div>
}
</div>
}
@if (isToolFailed(item.data)) {
<div class="expanded-error-banner">
<span class="material-symbols-outlined icon">error</span>
<span class="error-text">{{ getToolErrorMessage(item.data) }}</span>
</div>
}
</div>
}
</div>
}
}
}
</div>
}
<!-- Check block result: one row per check item, keyed by its number above -->
@if (hasCheckerResult(block)) {
<div class="check-result" [ngClass]="'tone-' + getCheckerTone(block)">
@for (verdict of getCheckerVerdicts(block); track $index) {
<div class="check-verdict" [ngClass]="verdict.status">
<span class="check-index">{{ getVerdictItemNumber(block, verdict, $index) }}</span>
<span class="material-symbols-outlined verdict-icon">{{ getVerdictIcon(verdict.status) }}</span>
<span class="verdict-body">
<span class="verdict-evidence">{{ verdict.evidence || getVerdictStatusText(verdict.status) }}</span>
@if (verdict.suggestion) {
<span class="verdict-suggestion">{{ verdict.suggestion }}</span>
}
</span>
</div>
}
@for (note of getCheckerNotes(block); track note) {
<div class="check-note">{{ note }}</div>
}
</div>
}
</div>
</div>
}
}
</div>
}
</div>
}
@if (awaitingGuidance().length) {
<div class="guidance-queue" aria-label="待处理用户指导">
@for (item of awaitingGuidance(); track item.id) {
<div class="guidance-queue-item">
<strong>用户指导 · {{ guidanceStatusLabel(item) }}</strong>
<p>{{ item.instruction }}</p>
</div>
}
</div>
}
@if (terminalFailureReason(); as reason) {
<div class="session-failure-summary" role="status">
<strong>任务执行失败 · 原因</strong>
<p>{{ reason }}</p>
<button type="button" (click)="isAutomationOpen.set(true)">查看完整报告</button>
</div>
}
<!-- Outputter Post-Execution Synthesis Card (Rendered ONCE at the end of stream) -->
@if (outputterReport(); as outReport) {
<div class="outputter-synthesis-stream-card">
<div class="synthesis-card-header">
<div class="header-left">
<span class="material-symbols-outlined icon-sparkle">assignment_turned_in</span>
<span class="main-title">任务报告</span>
</div>
<div class="header-right">
<button type="button" class="btn-open-note" (click)="openOutputterNote($event)">
<span class="material-symbols-outlined">visibility</span>
<span>在笔记中查看</span>
</button>
</div>
</div>
<div class="synthesis-card-preview">
@if (parsedOutputReport(); as parsedOut) {
<div class="parsed-note-container">
@if (parsedOut.title) {
<h3 class="parsed-note-title">{{ parsedOut.title }}</h3>
}
@if (parsedOut.milestones.length > 0) {
<div class="parsed-milestones">
@for (m of parsedOut.milestones; track trackMilestone($index, m)) {
<div class="milestone-item" [class]="'status-' + m.type">
<span class="material-symbols-outlined status-icon">
{{ m.type === 'checked' ? 'check_circle' : (m.type === 'progress' ? 'pending' : 'radio_button_unchecked') }}
</span>
<span class="milestone-text">
@for (seg of m.segments; track seg) {
@if (seg.bold) {
<strong>{{ seg.text }}</strong>
}
@if (seg.code) {
<code>{{ seg.text }}</code>
}
@if (!seg.bold && !seg.code) {
<span>{{ seg.text }}</span>
}
}
</span>
</div>
}
</div>
}
@if (parsedOut.otherLines.length > 0) {
<div class="parsed-other-lines">
@for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {
<p class="other-line" [class]="'line-' + line.type">
@if (line.type === 'verify') {
<span class="verify-badge" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }}</span>
}
@if (line.type === 'finding') {
<span class="finding-badge">finding</span>
}
@for (seg of line.segments; track seg) {
@if (seg.bold) {
<strong>{{ seg.text }}</strong>
}
@if (seg.code) {
<code>{{ seg.text }}</code>
}
@if (!seg.bold && !seg.code) {
<span>{{ seg.text }}</span>
}
}
</p>
}
</div>
}
</div>
}
</div>
</div>
}
<!-- Planning Loader -->
@if (showPlanningLoader() && (startupWorkItems().length === 0 || startupPreparationIsComplete() || consolidatedBlocks().length > 0)) {
<div class="planning-loader-container">
<div class="planning-loader">
<span class="material-symbols-outlined spin-animation">progress_activity</span>
<span class="loader-text">{{ currentPlanningText() }}</span>
</div>
</div>
}
</div>
}
</main>
<!-- Fullscreen Image Zoom Lightbox Modal -->
@if (selectedZoomImage()) {
<div class="image-modal-backdrop" (click)="closeImageModal($event)">
<div class="image-modal-content" (click)="$event.stopPropagation()">
<button class="image-modal-close" (click)="closeImageModal($event)" aria-label="关闭图片预览">
<span class="material-symbols-outlined">close</span>
</button>
<div class="img-relative-box">
<img #modalImg [src]="selectedZoomImage()" (load)="onImageLoad(modalImg, modalOverlay, selectedZoomAction())" alt="放大截图" />
<div #modalOverlay class="overlay-layer"></div>
</div>
</div>
</div>
}
</div>