Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/config/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"openai": "",
"anthropic": "",
"google": "",
"byteplus": "62a75ab1-0f00-4d4e-8873-23551e624375",
"byteplus": "",
"openrouter": ""
},
"endpoints": {
Expand Down
29 changes: 29 additions & 0 deletions app/ui_layer/browser/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/ui_layer/browser/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@reduxjs/toolkit": "^2.12.0",
"@tanstack/react-virtual": "^3.13.23",
"lucide-react": "^0.344.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,32 @@
font-style: italic;
}

/* Active-state placeholder shown at the end of a task's child action list
(Thinking… / Waiting for reply… / Paused…). Non-interactive except for
an always-visible reply button when the task is waiting. */
.placeholderItem {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
color: var(--text-muted);
font-style: italic;
}

/* Reply button on the placeholder — same look as .taskReplyBtn but always
visible (no hover-revealed opacity). */
.placeholderReplyBtn {
flex-shrink: 0;
color: var(--text-muted);
transition: color var(--transition-fast), background var(--transition-fast);
}

.placeholderReplyBtn:hover {
color: var(--color-primary);
background: var(--color-primary-light);
}

/* Hidden file input */
.hiddenFileInput {
display: none;
Expand Down
134 changes: 82 additions & 52 deletions app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { X, Loader2, Reply } from 'lucide-react'
import { useWebSocket } from '../../contexts/WebSocketContext'
import { IconButton, StatusIndicator } from '../../components/ui'
import { Chat } from '../../components/Chat'
import { MascotDisplay } from '@mascot'
import { getActivePlaceholder } from '../../utils/taskPlaceholder'
import styles from './ChatPage.module.css'

// Panel width limits
const DEFAULT_PANEL_WIDTH = 380
const DEFAULT_PANEL_WIDTH = 460
const MIN_PANEL_WIDTH = 200
const MAX_PANEL_WIDTH = 800

Expand Down Expand Up @@ -92,6 +94,7 @@ export function ChatPage() {

{/* Task/Action Panel */}
<div className={styles.actionPanel} style={{ width: panelWidth, flexShrink: 0 }}>
<MascotDisplay />
<div className={styles.panelHeader}>
<h3>Tasks & Actions</h3>
</div>
Expand All @@ -101,67 +104,94 @@ export function ChatPage() {
<p>No active tasks</p>
</div>
) : (
tasks.map(task => (
<div key={task.id} className={styles.taskGroup}>
<div
className={`${styles.taskItem} ${selectedTaskId === task.id ? styles.selected : ''}`}
onClick={() => setSelectedTaskId(
selectedTaskId === task.id ? null : task.id
)}
>
<StatusIndicator status={task.status} size="sm" />
<span className={styles.taskName}>{task.name}</span>
{(task.status === 'running' || task.status === 'waiting') && (
<>
{!tasksAwaitingOption.has(task.id) && (
tasks.map(task => {
const isExpanded = selectedTaskId === task.id
const taskActions = isExpanded ? getActionsForTask(task.id) : []
const listPlaceholder = isExpanded
? getActivePlaceholder(task.status, taskActions)
: null
const showListReply =
listPlaceholder?.status === 'waiting' && !tasksAwaitingOption.has(task.id)

return (
<div key={task.id} className={styles.taskGroup}>
<div
className={`${styles.taskItem} ${isExpanded ? styles.selected : ''}`}
onClick={() => setSelectedTaskId(isExpanded ? null : task.id)}
>
<StatusIndicator status={task.status} size="sm" />
<span className={styles.taskName}>{task.name}</span>
{(task.status === 'running' || task.status === 'waiting') && (
<>
{!tasksAwaitingOption.has(task.id) && (
<IconButton
size="sm"
variant="ghost"
className={styles.taskReplyBtn}
onClick={(e) => {
e.stopPropagation()
handleTaskReply(task.id, task.name)
}}
title="Reply to Task"
icon={<Reply size={12} />}
/>
)}
<IconButton
size="sm"
variant="ghost"
className={styles.taskReplyBtn}
className={styles.taskCancelBtn}
onClick={(e) => {
e.stopPropagation()
handleTaskReply(task.id, task.name)
cancelTask(task.id)
}}
title="Reply to Task"
icon={<Reply size={12} />}
disabled={cancellingTaskId === task.id}
title="Cancel Task"
icon={
cancellingTaskId === task.id ? (
<Loader2 size={12} className={styles.spinning} />
) : (
<X size={12} />
)
}
/>
</>
)}
</div>
{isExpanded && (
<div className={styles.actionsList}>
{taskActions.map(action => (
<div key={action.id} className={styles.actionItem}>
<StatusIndicator status={action.status} size="sm" />
<span className={styles.actionName}>{action.name}</span>
</div>
))}
{listPlaceholder && (
<div className={styles.placeholderItem}>
<StatusIndicator status={listPlaceholder.status} size="sm" />
<span className={styles.actionName}>{listPlaceholder.label}</span>
{showListReply && (
<IconButton
size="sm"
variant="ghost"
className={styles.placeholderReplyBtn}
onClick={(e) => {
e.stopPropagation()
handleTaskReply(task.id, task.name)
}}
title="Reply to Task"
icon={<Reply size={12} />}
/>
)}
</div>
)}
<IconButton
size="sm"
variant="ghost"
className={styles.taskCancelBtn}
onClick={(e) => {
e.stopPropagation()
cancelTask(task.id)
}}
disabled={cancellingTaskId === task.id}
title="Cancel Task"
icon={
cancellingTaskId === task.id ? (
<Loader2 size={12} className={styles.spinning} />
) : (
<X size={12} />
)
}
/>
</>
{taskActions.length === 0 && !listPlaceholder && (
<div className={styles.noActions}>No actions yet</div>
)}
</div>
)}
</div>
{selectedTaskId === task.id && (
<div className={styles.actionsList}>
{getActionsForTask(task.id).map(action => (
<div key={action.id} className={styles.actionItem}>
<StatusIndicator status={action.status} size="sm" />
<span className={styles.actionName}>{action.name}</span>
</div>
))}
{getActionsForTask(task.id).length === 0 && (
<div className={styles.noActions}>No actions yet</div>
)}
</div>
)}
</div>
))
)
})
)}
</div>
</div>
Expand Down
Loading