chore: ui folder structure
This commit is contained in:
601
packages/ui/src/panel/Panel.module.css
Normal file
601
packages/ui/src/panel/Panel.module.css
Normal file
@@ -0,0 +1,601 @@
|
||||
.wrapper {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
opacity: 0;
|
||||
z-index: 2147483642; /* 比 SimulatorMask 高一层 */
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow: visible;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
--width: 360px;
|
||||
--height: 40px;
|
||||
--border-radius: 12px;
|
||||
|
||||
--side-space: 12px; /* 控制栏两侧的间距 */
|
||||
--history-width: calc(var(--width) - var(--side-space) * 2);
|
||||
|
||||
--color-1: rgb(57, 182, 255);
|
||||
--color-2: rgb(189, 69, 251);
|
||||
--color-3: rgb(255, 87, 51);
|
||||
--color-4: rgb(255, 214, 0);
|
||||
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
width: calc(100vw - 40px);
|
||||
--width: calc(100vw - 40px);
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
inset: -2px -8px;
|
||||
border-radius: calc(var(--border-radius) + 4px);
|
||||
filter: blur(16px);
|
||||
overflow: hidden;
|
||||
/* mix-blend-mode: lighten; */
|
||||
/* display: none; */
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* left: -100%; */
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
background-image: linear-gradient(
|
||||
to bottom left,
|
||||
var(--color-1),
|
||||
var(--color-2),
|
||||
var(--color-1)
|
||||
);
|
||||
animation: mask-running 2s linear infinite;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
background-image: linear-gradient(
|
||||
to bottom left,
|
||||
var(--color-2),
|
||||
var(--color-1),
|
||||
var(--color-2)
|
||||
);
|
||||
animation: mask-running 2s linear infinite;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mask-running {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 控制栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
user-select: none;
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
cursor: pointer;
|
||||
flex-shrink: 0; /* 防止 header 被压缩 */
|
||||
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--border-radius);
|
||||
background-clip: padding-box;
|
||||
|
||||
box-shadow:
|
||||
0 0 0px 2px rgba(255, 255, 255, 0.4),
|
||||
0 0 5px 1px rgba(255, 255, 255, 0.3);
|
||||
|
||||
.statusSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 24px; /* 确保垂直居中 */
|
||||
|
||||
.indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
flex-shrink: 0;
|
||||
animation: none; /* 默认无动画 */
|
||||
|
||||
/* 运行状态 - 有动画 */
|
||||
&.thinking {
|
||||
background: rgb(57, 182, 255);
|
||||
animation: pulse 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.tool_executing {
|
||||
background: rgb(189, 69, 251);
|
||||
animation: pulse 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.retry {
|
||||
background: rgb(255, 214, 0);
|
||||
animation: retryPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 静止状态 - 无动画 */
|
||||
&.completed,
|
||||
&.input,
|
||||
&.output {
|
||||
background: rgb(34, 197, 94);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgb(239, 68, 68);
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.statusText {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 24px; /* 确保垂直居中 */
|
||||
|
||||
&.fadeOut {
|
||||
animation: statusTextFadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
&.fadeIn {
|
||||
animation: statusTextFadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.controlButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.stopButton {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: rgb(255, 41, 41);
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusTextFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusTextFadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.historySectionWrapper {
|
||||
position: absolute;
|
||||
width: var(--history-width);
|
||||
bottom: var(--height);
|
||||
left: var(--side-space);
|
||||
z-index: -2;
|
||||
|
||||
padding-top: 0px;
|
||||
visibility: collapse;
|
||||
overflow: hidden;
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
background: rgba(2, 0, 20, 0.5);
|
||||
/* background: rgba(186, 186, 186, 0.2); */
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
|
||||
border-top-left-radius: calc(var(--border-radius) + 4px);
|
||||
border-top-right-radius: calc(var(--border-radius) + 4px);
|
||||
|
||||
/* border: 2px solid rgba(255, 255, 255, 0.8); */
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(0, 0, 0, 0.85),
|
||||
0 2px 12px 0 rgba(57, 182, 255, 0.1);
|
||||
} */
|
||||
|
||||
.expanded & {
|
||||
padding-top: 8px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.historySection {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
max-height: 0;
|
||||
padding-inline: 8px;
|
||||
|
||||
transition: max-height 0.2s;
|
||||
|
||||
.expanded & {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.historyItem {
|
||||
/* backdrop-filter: blur(10px); */
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
|
||||
border-radius: 8px;
|
||||
border-left: 2px solid rgba(57, 182, 255, 0.5);
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
/* color: black; */
|
||||
line-height: 1.3;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* 微妙的内阴影 */
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));
|
||||
/* transform: translateY(-1px); */
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||
0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&.completed,
|
||||
&.input,
|
||||
&.output {
|
||||
border-left-color: rgb(34, 197, 94);
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-left-color: rgb(239, 68, 68);
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05));
|
||||
}
|
||||
|
||||
&.retry {
|
||||
border-left-color: rgb(255, 214, 0);
|
||||
background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));
|
||||
}
|
||||
|
||||
&.observation {
|
||||
border-left-color: rgb(147, 51, 234);
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));
|
||||
}
|
||||
|
||||
&.question {
|
||||
border-left-color: rgb(255, 159, 67);
|
||||
background: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));
|
||||
}
|
||||
|
||||
/* 突出显示 done 成功结果 */
|
||||
&.doneSuccess {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(34, 197, 94, 0.25),
|
||||
rgba(34, 197, 94, 0.15),
|
||||
rgba(34, 197, 94, 0.08)
|
||||
);
|
||||
border: none;
|
||||
border-left: 4px solid rgb(34, 197, 94);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(34, 197, 94, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 0 20px rgba(34, 197, 94, 0.1);
|
||||
font-weight: 600;
|
||||
color: rgb(220, 252, 231);
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.4), transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.historyContent {
|
||||
.statusIcon {
|
||||
font-size: 16px;
|
||||
animation: celebrate 0.8s ease-in-out;
|
||||
filter: drop-shadow(0 2px 4px rgba(34, 197, 94, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 突出显示 done 失败结果 */
|
||||
&.doneError {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(239, 68, 68, 0.25),
|
||||
rgba(239, 68, 68, 0.15),
|
||||
rgba(239, 68, 68, 0.08)
|
||||
);
|
||||
border: none;
|
||||
border-left: 4px solid rgb(239, 68, 68);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(239, 68, 68, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 0 20px rgba(239, 68, 68, 0.1);
|
||||
font-weight: 600;
|
||||
color: rgb(254, 226, 226);
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(90deg, transparent, rgba(239, 68, 68, 0.4), transparent);
|
||||
}
|
||||
|
||||
.historyContent {
|
||||
.statusIcon {
|
||||
font-size: 16px;
|
||||
filter: drop-shadow(0 2px 4px rgba(239, 68, 68, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.historyContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
|
||||
/* overflow-x: auto; */
|
||||
|
||||
.statusIcon {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.reflectionLines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.historyMeta {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
/* color: rgb(61, 61, 61); */
|
||||
margin-top: 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画关键帧 - 更快的闪烁 */
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 重试动画 - 旋转脉冲 */
|
||||
@keyframes retryPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2) rotate(90deg);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1) rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2) rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 庆祝动画 */
|
||||
@keyframes celebrate {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.2) rotate(-5deg);
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.2) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* done 卡片的光泽效果 */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入区域样式 */
|
||||
.inputSectionWrapper {
|
||||
position: absolute;
|
||||
width: var(--history-width);
|
||||
top: var(--height);
|
||||
left: var(--side-space);
|
||||
z-index: -1;
|
||||
|
||||
visibility: visible;
|
||||
overflow: hidden;
|
||||
|
||||
height: 48px;
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
background: rgba(186, 186, 186, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
border-bottom-left-radius: calc(var(--border-radius) + 4px);
|
||||
border-bottom-right-radius: calc(var(--border-radius) + 4px);
|
||||
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 1px 16px rgba(0, 0, 0, 0.4);
|
||||
|
||||
&.hidden {
|
||||
visibility: collapse;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.inputSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 8px;
|
||||
|
||||
.taskInput {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
padding-inline: 10px;
|
||||
color: rgb(20, 20, 20);
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
line-height: 1;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
/* text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); */
|
||||
|
||||
/* border-color: rgba(57, 182, 255, 0.3); */
|
||||
|
||||
&::placeholder {
|
||||
color: rgb(53, 53, 53);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(57, 182, 255, 0.6);
|
||||
box-shadow: 0 0 0 2px rgba(57, 182, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
649
packages/ui/src/panel/Panel.ts
Normal file
649
packages/ui/src/panel/Panel.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
import { I18n, type SupportedLanguage } from '../i18n'
|
||||
import { truncate } from '../utils'
|
||||
import { createCard, createReflectionLines, formatTime } from './cards'
|
||||
import type { AgentActivity, PanelAgentAdapter } from './types'
|
||||
|
||||
import styles from './Panel.module.css'
|
||||
|
||||
/**
|
||||
* Panel configuration
|
||||
*/
|
||||
export interface PanelConfig {
|
||||
language?: SupportedLanguage
|
||||
/**
|
||||
* Whether to prompt for next task after task completion
|
||||
* @default true
|
||||
*/
|
||||
promptForNextTask?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent control panel
|
||||
*
|
||||
* Architecture:
|
||||
* - History list: renders directly from agent.history (historical events)
|
||||
* - Header bar: shows activity events (transient state) and agent status
|
||||
*
|
||||
* This separation ensures data consistency - history is the single source of truth
|
||||
* for what has been done, while activity shows what is happening now.
|
||||
*/
|
||||
export class Panel {
|
||||
#wrapper: HTMLElement
|
||||
#indicator: HTMLElement
|
||||
#statusText: HTMLElement
|
||||
#historySection: HTMLElement
|
||||
#expandButton: HTMLElement
|
||||
#stopButton: HTMLElement
|
||||
#inputSection: HTMLElement
|
||||
#taskInput: HTMLInputElement
|
||||
|
||||
#agent: PanelAgentAdapter
|
||||
#config: PanelConfig
|
||||
#isExpanded = false
|
||||
#i18n: I18n
|
||||
#userAnswerResolver: ((input: string) => void) | null = null
|
||||
#isWaitingForUserAnswer: boolean = false
|
||||
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null
|
||||
#pendingHeaderText: string | null = null
|
||||
#isAnimating = false
|
||||
|
||||
// Event handlers (bound for removal)
|
||||
#onStatusChange = () => this.#handleStatusChange()
|
||||
#onHistoryChange = () => this.#handleHistoryChange()
|
||||
#onActivity = (e: Event) => this.#handleActivity((e as CustomEvent<AgentActivity>).detail)
|
||||
#onAgentDispose = () => this.dispose()
|
||||
|
||||
get wrapper(): HTMLElement {
|
||||
return this.#wrapper
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Panel bound to an agent
|
||||
* @param agent - Agent instance that implements PanelAgentAdapter
|
||||
* @param config - Optional panel configuration
|
||||
*/
|
||||
constructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {
|
||||
this.#agent = agent
|
||||
this.#config = config
|
||||
this.#i18n = new I18n(config.language ?? 'en-US')
|
||||
|
||||
// Set up askUser callback on agent
|
||||
this.#agent.onAskUser = (question) => this.#askUser(question)
|
||||
|
||||
// Create UI elements
|
||||
this.#wrapper = this.#createWrapper()
|
||||
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
|
||||
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
||||
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
||||
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
|
||||
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
|
||||
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
|
||||
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
||||
|
||||
// Listen to agent events
|
||||
this.#agent.addEventListener('statuschange', this.#onStatusChange)
|
||||
this.#agent.addEventListener('historychange', this.#onHistoryChange)
|
||||
this.#agent.addEventListener('activity', this.#onActivity)
|
||||
this.#agent.addEventListener('dispose', this.#onAgentDispose)
|
||||
|
||||
this.#setupEventListeners()
|
||||
this.#startHeaderUpdateLoop()
|
||||
|
||||
this.#showInputArea()
|
||||
|
||||
this.hide() // Start hidden
|
||||
}
|
||||
|
||||
// ========== Agent event handlers ==========
|
||||
|
||||
/** Handle agent status change */
|
||||
#handleStatusChange(): void {
|
||||
const status = this.#agent.status
|
||||
|
||||
// Map agent status to UI indicator type
|
||||
const indicatorType =
|
||||
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
|
||||
this.#updateStatusIndicator(indicatorType)
|
||||
|
||||
// Show/hide based on status
|
||||
if (status === 'running') {
|
||||
this.show()
|
||||
this.#hideInputArea() // Hide input while running
|
||||
}
|
||||
|
||||
// Handle completion
|
||||
if (status === 'completed' || status === 'error') {
|
||||
if (!this.#isExpanded) {
|
||||
this.#expand()
|
||||
}
|
||||
if (this.#shouldShowInputArea()) {
|
||||
this.#showInputArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle agent history change - re-render history list from agent.history */
|
||||
#handleHistoryChange(): void {
|
||||
this.#renderHistory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agent activity - transient state for immediate UI feedback
|
||||
* Activity events are NOT persisted in history, only used for header bar updates
|
||||
*/
|
||||
#handleActivity(activity: AgentActivity): void {
|
||||
switch (activity.type) {
|
||||
case 'thinking':
|
||||
this.#pendingHeaderText = this.#i18n.t('ui.panel.thinking')
|
||||
this.#updateStatusIndicator('thinking')
|
||||
break
|
||||
|
||||
case 'executing':
|
||||
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input)
|
||||
this.#updateStatusIndicator('executing')
|
||||
break
|
||||
|
||||
case 'executed':
|
||||
this.#pendingHeaderText = truncate(activity.output, 50)
|
||||
break
|
||||
|
||||
case 'retrying':
|
||||
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`
|
||||
this.#updateStatusIndicator('retrying')
|
||||
break
|
||||
|
||||
case 'error':
|
||||
this.#pendingHeaderText = truncate(activity.message, 50)
|
||||
this.#updateStatusIndicator('error')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for user input (internal, called by agent via onAskUser)
|
||||
*/
|
||||
#askUser(question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
// Set `waiting for user answer` state
|
||||
this.#isWaitingForUserAnswer = true
|
||||
this.#userAnswerResolver = resolve
|
||||
|
||||
// Expand history panel
|
||||
if (!this.#isExpanded) {
|
||||
this.#expand()
|
||||
}
|
||||
|
||||
// Add temporary question card so user can see the full question
|
||||
const tempCard = document.createElement('div')
|
||||
tempCard.innerHTML = createCard({
|
||||
icon: '❓',
|
||||
content: `Question: ${question}`,
|
||||
meta: formatTime(),
|
||||
type: 'question',
|
||||
})
|
||||
const cardElement = tempCard.firstElementChild as HTMLElement
|
||||
cardElement.setAttribute('data-temp-card', 'true')
|
||||
this.#historySection.appendChild(cardElement)
|
||||
this.#scrollToBottom()
|
||||
|
||||
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
|
||||
})
|
||||
}
|
||||
|
||||
// ========== Public control methods ==========
|
||||
|
||||
show(): void {
|
||||
this.wrapper.style.display = 'block'
|
||||
void this.wrapper.offsetHeight
|
||||
this.wrapper.style.opacity = '1'
|
||||
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.wrapper.style.opacity = '0'
|
||||
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
|
||||
this.wrapper.style.display = 'none'
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
|
||||
this.#updateStatusIndicator('thinking')
|
||||
this.#renderHistory()
|
||||
this.#collapse()
|
||||
// Reset user input state
|
||||
this.#isWaitingForUserAnswer = false
|
||||
this.#userAnswerResolver = null
|
||||
// Show input area
|
||||
this.#showInputArea()
|
||||
}
|
||||
|
||||
expand(): void {
|
||||
this.#expand()
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
this.#collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose panel and clean up event listeners
|
||||
*/
|
||||
dispose(): void {
|
||||
// Remove agent event listeners
|
||||
this.#agent.removeEventListener('statuschange', this.#onStatusChange)
|
||||
this.#agent.removeEventListener('historychange', this.#onHistoryChange)
|
||||
this.#agent.removeEventListener('activity', this.#onActivity)
|
||||
this.#agent.removeEventListener('dispose', this.#onAgentDispose)
|
||||
|
||||
// Clean up UI
|
||||
this.#isWaitingForUserAnswer = false
|
||||
this.#stopHeaderUpdateLoop()
|
||||
this.wrapper.remove()
|
||||
}
|
||||
|
||||
// ========== Private methods ==========
|
||||
|
||||
#getToolExecutingText(toolName: string, args: unknown): string {
|
||||
const a = args as Record<string, string | number>
|
||||
switch (toolName) {
|
||||
case 'click_element_by_index':
|
||||
return this.#i18n.t('ui.tools.clicking', { index: a.index })
|
||||
case 'input_text':
|
||||
return this.#i18n.t('ui.tools.inputting', { index: a.index })
|
||||
case 'select_dropdown_option':
|
||||
return this.#i18n.t('ui.tools.selecting', { text: a.text })
|
||||
case 'scroll':
|
||||
return this.#i18n.t('ui.tools.scrolling')
|
||||
case 'wait':
|
||||
return this.#i18n.t('ui.tools.waiting', { seconds: a.seconds })
|
||||
case 'ask_user':
|
||||
return this.#i18n.t('ui.tools.askingUser')
|
||||
case 'done':
|
||||
return this.#i18n.t('ui.tools.done')
|
||||
default:
|
||||
return this.#i18n.t('ui.tools.executing', { toolName })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Agent
|
||||
*/
|
||||
#stopAgent(): void {
|
||||
this.#agent.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit task
|
||||
*/
|
||||
#submitTask() {
|
||||
const input = this.#taskInput.value.trim()
|
||||
if (!input) return
|
||||
|
||||
// Hide input area
|
||||
this.#hideInputArea()
|
||||
|
||||
if (this.#isWaitingForUserAnswer) {
|
||||
// Handle user input mode
|
||||
this.#handleUserAnswer(input)
|
||||
} else {
|
||||
// Execute task via agent
|
||||
this.#agent.execute(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user answer
|
||||
*/
|
||||
#handleUserAnswer(input: string): void {
|
||||
// Remove temporary question cards (only direct children for safety)
|
||||
Array.from(this.#historySection.children).forEach((child) => {
|
||||
if (child.getAttribute('data-temp-card') === 'true') {
|
||||
child.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// Reset state
|
||||
this.#isWaitingForUserAnswer = false
|
||||
|
||||
// Call resolver to return user input
|
||||
if (this.#userAnswerResolver) {
|
||||
this.#userAnswerResolver(input)
|
||||
this.#userAnswerResolver = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show input area
|
||||
*/
|
||||
#showInputArea(placeholder?: string): void {
|
||||
// Clear input field
|
||||
this.#taskInput.value = ''
|
||||
this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')
|
||||
this.#inputSection.classList.remove(styles.hidden)
|
||||
// Focus on input field
|
||||
setTimeout(() => {
|
||||
this.#taskInput.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide input area
|
||||
*/
|
||||
#hideInputArea(): void {
|
||||
this.#inputSection.classList.add(styles.hidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input area should be shown
|
||||
*/
|
||||
#shouldShowInputArea(): boolean {
|
||||
// Always show input area if waiting for user input
|
||||
if (this.#isWaitingForUserAnswer) return true
|
||||
|
||||
const history = this.#agent.history
|
||||
if (history.length === 0) {
|
||||
return true // Initial state
|
||||
}
|
||||
|
||||
const status = this.#agent.status
|
||||
const isTaskEnded = status === 'completed' || status === 'error'
|
||||
|
||||
// Only show input area after task completion if configured to do so
|
||||
if (isTaskEnded) {
|
||||
return this.#config.promptForNextTask ?? true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
#createWrapper(): HTMLElement {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.id = 'page-agent-runtime_agent-panel'
|
||||
wrapper.className = styles.wrapper
|
||||
wrapper.setAttribute('data-browser-use-ignore', 'true')
|
||||
|
||||
wrapper.innerHTML = `
|
||||
<div class="${styles.background}"></div>
|
||||
<div class="${styles.historySectionWrapper}">
|
||||
<div class="${styles.historySection}">
|
||||
<div class="${styles.historyItem}">
|
||||
<div class="${styles.historyContent}">
|
||||
<span class="${styles.statusIcon}">🧠</span>
|
||||
<span>${this.#i18n.t('ui.panel.waitingPlaceholder')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="${styles.header}">
|
||||
<div class="${styles.statusSection}">
|
||||
<div class="${styles.indicator} ${styles.thinking}"></div>
|
||||
<div class="${styles.statusText}">${this.#i18n.t('ui.panel.ready')}</div>
|
||||
</div>
|
||||
<div class="${styles.controls}">
|
||||
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
||||
▼
|
||||
</button>
|
||||
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="${styles.inputSectionWrapper} ${styles.hidden}">
|
||||
<div class="${styles.inputSection}">
|
||||
<input
|
||||
type="text"
|
||||
class="${styles.taskInput}"
|
||||
maxlength="200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
document.body.appendChild(wrapper)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
#setupEventListeners(): void {
|
||||
// Click header area to expand/collapse
|
||||
const header = this.wrapper.querySelector(`.${styles.header}`)!
|
||||
header.addEventListener('click', (e) => {
|
||||
// Don't trigger expand/collapse if clicking on buttons
|
||||
if ((e.target as HTMLElement).closest(`.${styles.controlButton}`)) {
|
||||
return
|
||||
}
|
||||
this.#toggle()
|
||||
})
|
||||
|
||||
// Expand button
|
||||
this.#expandButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
this.#toggle()
|
||||
})
|
||||
|
||||
// Stop button
|
||||
this.#stopButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
this.#stopAgent()
|
||||
})
|
||||
|
||||
// Submit on Enter key in input field
|
||||
this.#taskInput.addEventListener('keydown', (e) => {
|
||||
if (e.isComposing) return // Ignore IME composition keys
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
this.#submitTask()
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent input area click event bubbling
|
||||
this.#inputSection.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
}
|
||||
|
||||
#toggle(): void {
|
||||
if (this.#isExpanded) {
|
||||
this.#collapse()
|
||||
} else {
|
||||
this.#expand()
|
||||
}
|
||||
}
|
||||
|
||||
#expand(): void {
|
||||
this.#isExpanded = true
|
||||
this.wrapper.classList.add(styles.expanded)
|
||||
this.#expandButton.textContent = '▲'
|
||||
}
|
||||
|
||||
#collapse(): void {
|
||||
this.#isExpanded = false
|
||||
this.wrapper.classList.remove(styles.expanded)
|
||||
this.#expandButton.textContent = '▼'
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic header update loop
|
||||
*/
|
||||
#startHeaderUpdateLoop(): void {
|
||||
// Check every 450ms (same as total animation duration)
|
||||
this.#headerUpdateTimer = setInterval(() => {
|
||||
this.#checkAndUpdateHeader()
|
||||
}, 450)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic header update loop
|
||||
*/
|
||||
#stopHeaderUpdateLoop(): void {
|
||||
if (this.#headerUpdateTimer) {
|
||||
clearInterval(this.#headerUpdateTimer)
|
||||
this.#headerUpdateTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if header needs update and trigger animation if not currently animating
|
||||
*/
|
||||
#checkAndUpdateHeader(): void {
|
||||
// If no pending text or currently animating, skip
|
||||
if (!this.#pendingHeaderText || this.#isAnimating) {
|
||||
return
|
||||
}
|
||||
|
||||
// If text is already displayed, clear pending and skip
|
||||
if (this.#statusText.textContent === this.#pendingHeaderText) {
|
||||
this.#pendingHeaderText = null
|
||||
return
|
||||
}
|
||||
|
||||
// Start animation
|
||||
const textToShow = this.#pendingHeaderText
|
||||
this.#pendingHeaderText = null
|
||||
this.#animateTextChange(textToShow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate text change with fade out/in effect
|
||||
*/
|
||||
#animateTextChange(newText: string): void {
|
||||
this.#isAnimating = true
|
||||
|
||||
// Fade out current text
|
||||
this.#statusText.classList.add(styles.fadeOut)
|
||||
|
||||
setTimeout(() => {
|
||||
// Update text content
|
||||
this.#statusText.textContent = newText
|
||||
|
||||
// Fade in new text
|
||||
this.#statusText.classList.remove(styles.fadeOut)
|
||||
this.#statusText.classList.add(styles.fadeIn)
|
||||
|
||||
setTimeout(() => {
|
||||
this.#statusText.classList.remove(styles.fadeIn)
|
||||
this.#isAnimating = false
|
||||
}, 300)
|
||||
}, 150) // Half the duration of fade out animation
|
||||
}
|
||||
|
||||
#updateStatusIndicator(
|
||||
type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'
|
||||
): void {
|
||||
// Clear all status classes
|
||||
this.#indicator.className = styles.indicator
|
||||
|
||||
// Add corresponding status class
|
||||
this.#indicator.classList.add(styles[type])
|
||||
}
|
||||
|
||||
#scrollToBottom(): void {
|
||||
// Execute in next event loop to ensure DOM update completion
|
||||
setTimeout(() => {
|
||||
this.#historySection.scrollTop = this.#historySection.scrollHeight
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render history directly from agent.history
|
||||
*
|
||||
* Renders:
|
||||
* 1. Task (first item, from agent.task)
|
||||
* 2. Reflection cards (evaluation, memory, next_goal)
|
||||
* 3. Tool execution with output
|
||||
* 4. Observations
|
||||
*/
|
||||
#renderHistory(): void {
|
||||
const items: string[] = []
|
||||
|
||||
// 1. Task card (always first)
|
||||
const task = this.#agent.task
|
||||
if (task) {
|
||||
items.push(this.#createTaskCard(task))
|
||||
}
|
||||
|
||||
// 2. Render each history event
|
||||
const history = this.#agent.history
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const event = history[i]
|
||||
items.push(...this.#createHistoryCards(event, i + 1))
|
||||
}
|
||||
|
||||
this.#historySection.innerHTML = items.join('')
|
||||
this.#scrollToBottom()
|
||||
}
|
||||
|
||||
#createTaskCard(task: string): string {
|
||||
return createCard({ icon: '🎯', content: task, type: 'input' })
|
||||
}
|
||||
|
||||
/** Create cards for a history event */
|
||||
#createHistoryCards(event: PanelAgentAdapter['history'][number], stepNumber: number): string[] {
|
||||
const cards: string[] = []
|
||||
const time = formatTime()
|
||||
const meta = this.#i18n.t('ui.panel.step', {
|
||||
number: stepNumber.toString(),
|
||||
time,
|
||||
duration: '',
|
||||
})
|
||||
|
||||
if (event.type === 'step') {
|
||||
// Reflection card
|
||||
if (event.reflection) {
|
||||
const lines = createReflectionLines(event.reflection)
|
||||
if (lines.length > 0) {
|
||||
cards.push(createCard({ icon: '🧠', content: lines, meta }))
|
||||
}
|
||||
}
|
||||
|
||||
// Action card
|
||||
const action = event.action
|
||||
if (action) {
|
||||
cards.push(...this.#createActionCards(action, meta))
|
||||
}
|
||||
} else if (event.type === 'observation') {
|
||||
cards.push(
|
||||
createCard({ icon: '👁️', content: event.content || '', meta, type: 'observation' })
|
||||
)
|
||||
} else if (event.type === 'user_takeover') {
|
||||
cards.push(createCard({ icon: '👤', content: 'User takeover', meta, type: 'input' }))
|
||||
}
|
||||
|
||||
return cards
|
||||
}
|
||||
|
||||
/** Create cards for an action */
|
||||
#createActionCards(
|
||||
action: { name: string; input: unknown; output: string },
|
||||
meta: string
|
||||
): string[] {
|
||||
const cards: string[] = []
|
||||
|
||||
if (action.name === 'done') {
|
||||
const input = action.input as { text?: string }
|
||||
const text = input.text || action.output || ''
|
||||
if (text) {
|
||||
cards.push(createCard({ icon: '🤖', content: text, meta, type: 'output' }))
|
||||
}
|
||||
} else if (action.name === 'ask_user') {
|
||||
const input = action.input as { question?: string }
|
||||
const answer = action.output.replace(/^User answered:\s*/i, '')
|
||||
cards.push(
|
||||
createCard({
|
||||
icon: '❓',
|
||||
content: `Question: ${input.question || ''}`,
|
||||
meta,
|
||||
type: 'question',
|
||||
})
|
||||
)
|
||||
cards.push(createCard({ icon: '💬', content: `Answer: ${answer}`, meta, type: 'input' }))
|
||||
} else {
|
||||
const toolText = this.#getToolExecutingText(action.name, action.input)
|
||||
cards.push(createCard({ icon: '🔨', content: toolText, meta }))
|
||||
if (action.output?.length > 0) {
|
||||
cards.push(createCard({ icon: '🔨', content: action.output, meta, type: 'output' }))
|
||||
}
|
||||
}
|
||||
|
||||
return cards
|
||||
}
|
||||
}
|
||||
62
packages/ui/src/panel/cards.ts
Normal file
62
packages/ui/src/panel/cards.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Card HTML generation utilities for Panel
|
||||
*/
|
||||
import { escapeHtml } from '../utils'
|
||||
|
||||
import styles from './Panel.module.css'
|
||||
|
||||
type CardType = 'default' | 'input' | 'output' | 'question' | 'observation'
|
||||
|
||||
interface CardOptions {
|
||||
icon: string
|
||||
content: string | string[]
|
||||
meta?: string
|
||||
type?: CardType
|
||||
}
|
||||
|
||||
/** Create a single history card */
|
||||
export function createCard({ icon, content, meta, type }: CardOptions): string {
|
||||
const typeClass = type ? styles[type] : ''
|
||||
const contentHtml = Array.isArray(content)
|
||||
? `<div class="${styles.reflectionLines}">${content.join('')}</div>`
|
||||
: `<span>${escapeHtml(content)}</span>`
|
||||
|
||||
return `
|
||||
<div class="${styles.historyItem} ${typeClass}">
|
||||
<div class="${styles.historyContent}">
|
||||
<span class="${styles.statusIcon}">${icon}</span>
|
||||
${contentHtml}
|
||||
</div>
|
||||
${meta ? `<div class="${styles.historyMeta}">${meta}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
/** Format timestamp for cards */
|
||||
export function formatTime(): string {
|
||||
return new Date().toLocaleTimeString('zh-CN', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
/** Create reflection lines from reflection object */
|
||||
export function createReflectionLines(reflection: {
|
||||
evaluation_previous_goal?: string
|
||||
memory?: string
|
||||
next_goal?: string
|
||||
}): string[] {
|
||||
const lines: string[] = []
|
||||
if (reflection.evaluation_previous_goal) {
|
||||
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`)
|
||||
}
|
||||
if (reflection.memory) {
|
||||
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`)
|
||||
}
|
||||
if (reflection.next_goal) {
|
||||
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
67
packages/ui/src/panel/types.ts
Normal file
67
packages/ui/src/panel/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Agent activity - transient state for immediate UI feedback.
|
||||
*
|
||||
* Unlike historical events (which are persisted), activities are ephemeral
|
||||
* and represent "what the agent is doing right now". UI components should
|
||||
* listen to 'activity' events to show real-time feedback.
|
||||
*
|
||||
* Note: There is no 'idle' activity - absence of activity events means idle.
|
||||
*
|
||||
* Events dispatched: CustomEvent<AgentActivity>
|
||||
*/
|
||||
export type AgentActivity =
|
||||
| { type: 'thinking' }
|
||||
| { type: 'executing'; tool: string; input: unknown }
|
||||
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
||||
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
||||
| { type: 'error'; message: string }
|
||||
|
||||
/**
|
||||
* Minimal interface that Panel expects from an agent.
|
||||
* Panel does not depend on PageAgent directly - it only requires this interface.
|
||||
* This enables decoupling and allows any agent implementation to work with Panel.
|
||||
*
|
||||
* Events:
|
||||
* - 'statuschange': Agent status changed (idle/running/completed/error)
|
||||
* - 'historychange': Historical events updated (persisted)
|
||||
* - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)
|
||||
* - 'dispose': Agent is being disposed
|
||||
*/
|
||||
export interface PanelAgentAdapter extends EventTarget {
|
||||
/** Current agent status */
|
||||
readonly status: 'idle' | 'running' | 'completed' | 'error'
|
||||
|
||||
/** History of agent events */
|
||||
readonly history: readonly {
|
||||
type: 'step' | 'observation' | 'user_takeover' | 'error'
|
||||
/** For 'step' type */
|
||||
reflection?: {
|
||||
evaluation_previous_goal?: string
|
||||
memory?: string
|
||||
next_goal?: string
|
||||
}
|
||||
/** For 'step' type */
|
||||
action?: {
|
||||
name: string
|
||||
input: unknown
|
||||
output: string
|
||||
}
|
||||
/** For 'observation' type */
|
||||
content?: string
|
||||
}[]
|
||||
|
||||
/** Current task being executed */
|
||||
readonly task: string
|
||||
|
||||
/**
|
||||
* Callback for when agent needs user input.
|
||||
* Panel will set this to handle user questions via its UI.
|
||||
*/
|
||||
onAskUser?: (question: string) => Promise<string>
|
||||
|
||||
/** Execute a task */
|
||||
execute(task: string): Promise<unknown>
|
||||
|
||||
/** Dispose the agent */
|
||||
dispose(): void
|
||||
}
|
||||
Reference in New Issue
Block a user