diff --git a/app/config/settings.json b/app/config/settings.json
index 22101bbac..65e95adaa 100644
--- a/app/config/settings.json
+++ b/app/config/settings.json
@@ -25,7 +25,7 @@
"openai": "",
"anthropic": "",
"google": "",
- "byteplus": "62a75ab1-0f00-4d4e-8873-23551e624375",
+ "byteplus": "",
"openrouter": ""
},
"endpoints": {
diff --git a/app/ui_layer/browser/frontend/package-lock.json b/app/ui_layer/browser/frontend/package-lock.json
index 94b51372b..7ef85425e 100644
--- a/app/ui_layer/browser/frontend/package-lock.json
+++ b/app/ui_layer/browser/frontend/package-lock.json
@@ -11,6 +11,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",
@@ -1486,6 +1487,12 @@
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.6",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
+ "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -2012,6 +2019,15 @@
"url": "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/sponsors/wooorm"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4243,6 +4259,19 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prism-react-renderer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
+ "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prismjs": "^1.26.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0"
+ }
+ },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
diff --git a/app/ui_layer/browser/frontend/package.json b/app/ui_layer/browser/frontend/package.json
index 214bc4d0a..33d5584b1 100644
--- a/app/ui_layer/browser/frontend/package.json
+++ b/app/ui_layer/browser/frontend/package.json
@@ -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",
diff --git a/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_day.jpg b/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_day.jpg
new file mode 100644
index 000000000..a8cc2c6b5
Binary files /dev/null and b/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_day.jpg differ
diff --git a/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_night.jpg b/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_night.jpg
new file mode 100644
index 000000000..cec60bf20
Binary files /dev/null and b/app/ui_layer/browser/frontend/public/mascot-backgrounds/background_1_night.jpg differ
diff --git a/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.module.css b/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.module.css
index d9c2d34ae..035444c89 100644
--- a/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.module.css
+++ b/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.module.css
@@ -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;
diff --git a/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.tsx b/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.tsx
index 149765721..a20cacc84 100644
--- a/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.tsx
+++ b/app/ui_layer/browser/frontend/src/pages/Chat/ChatPage.tsx
@@ -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
@@ -92,6 +94,7 @@ export function ChatPage() {
{/* Task/Action Panel */}
+
Tasks & Actions
@@ -101,67 +104,94 @@ export function ChatPage() {
No active tasks
) : (
- tasks.map(task => (
-
-
setSelectedTaskId(
- selectedTaskId === task.id ? null : task.id
- )}
- >
-
-
{task.name}
- {(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 (
+
+
setSelectedTaskId(isExpanded ? null : task.id)}
+ >
+
+ {task.name}
+ {(task.status === 'running' || task.status === 'waiting') && (
+ <>
+ {!tasksAwaitingOption.has(task.id) && (
+ {
+ e.stopPropagation()
+ handleTaskReply(task.id, task.name)
+ }}
+ title="Reply to Task"
+ icon={}
+ />
+ )}
{
e.stopPropagation()
- handleTaskReply(task.id, task.name)
+ cancelTask(task.id)
}}
- title="Reply to Task"
- icon={}
+ disabled={cancellingTaskId === task.id}
+ title="Cancel Task"
+ icon={
+ cancellingTaskId === task.id ? (
+
+ ) : (
+
+ )
+ }
/>
+ >
+ )}
+
+ {isExpanded && (
+
+ {taskActions.map(action => (
+
+
+ {action.name}
+
+ ))}
+ {listPlaceholder && (
+
+
+ {listPlaceholder.label}
+ {showListReply && (
+ {
+ e.stopPropagation()
+ handleTaskReply(task.id, task.name)
+ }}
+ title="Reply to Task"
+ icon={}
+ />
+ )}
+
)}
-
{
- e.stopPropagation()
- cancelTask(task.id)
- }}
- disabled={cancellingTaskId === task.id}
- title="Cancel Task"
- icon={
- cancellingTaskId === task.id ? (
-
- ) : (
-
- )
- }
- />
- >
+ {taskActions.length === 0 && !listPlaceholder && (
+ No actions yet
+ )}
+
)}
- {selectedTaskId === task.id && (
-
- {getActionsForTask(task.id).map(action => (
-
-
- {action.name}
-
- ))}
- {getActionsForTask(task.id).length === 0 && (
-
No actions yet
- )}
-
- )}
-
- ))
+ )
+ })
)}
diff --git a/app/ui_layer/browser/frontend/src/pages/LivingUI/CraftBotPet.tsx b/app/ui_layer/browser/frontend/src/pages/LivingUI/CraftBotPet.tsx
deleted file mode 100644
index 2f6103335..000000000
--- a/app/ui_layer/browser/frontend/src/pages/LivingUI/CraftBotPet.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-import styles from './LivingUIPage.module.css'
-
-export type PetState = 'creating' | 'launching' | 'stopped'
-
-interface Props {
- state: PetState
- progress?: number
- indeterminate?: boolean
- completedCount?: number
- size?: number
-}
-
-// Eye path d-values lifted from the original logo (paths 3 and 2 in the SVG).
-// Keeping them in their own wrappers lets us apply CSS transforms (blink/close)
-// without fighting the SVG-attribute transforms on the paths themselves.
-const LEFT_EYE_D =
- 'M0 0 C2.92937654 1.81310142 5.43647949 3.87295899 7 7 C7.12311126 9.56761586 7.18836415 12.10706019 7.203125 14.67578125 C7.20882507 15.43795456 7.21452515 16.20012787 7.22039795 16.98539734 C7.22985081 18.60019542 7.23638307 20.21501282 7.24023438 21.82983398 C7.24992133 24.29250092 7.28093303 26.75432656 7.3125 29.21679688 C7.3190321 30.78645338 7.32428176 32.35611586 7.328125 33.92578125 C7.3404718 34.65928299 7.3528186 35.39278473 7.36553955 36.14851379 C7.34154924 41.69538739 6.08230904 44.91769096 2.1875 48.8125 C-0.8527265 50.46290867 -2.57949832 50.54728027 -6 50 C-8.9228334 48.16899068 -11.43323854 46.13352292 -13 43 C-13.28424778 37.93535225 -13.31377183 32.86972252 -13.35009766 27.79785156 C-13.36663059 26.1063051 -13.39388581 24.41482773 -13.43212891 22.72363281 C-13.48710365 20.27893001 -13.50880304 17.835864 -13.5234375 15.390625 C-13.54610687 14.64041077 -13.56877625 13.89019653 -13.59213257 13.11724854 C-13.57625216 9.13973933 -13.06467198 6.59267156 -10.59228516 3.53808594 C-6.97219204 -0.00631866 -5.04757551 -0.58241256 0 0 Z'
-
-const RIGHT_EYE_D =
- 'M0 0 C4.33669495 0.36139125 5.72030822 1.28280822 8.75 4.3125 C11.19715759 7.98323639 11.07300278 10.80084797 11.0859375 15.05078125 C11.0925943 15.80916794 11.0992511 16.56755463 11.10610962 17.34892273 C11.11623038 18.95353778 11.12092682 20.55819486 11.12060547 22.1628418 C11.12496613 24.60602159 11.16125324 27.04737571 11.19921875 29.49023438 C11.20508977 31.05207718 11.20905726 32.61392854 11.2109375 34.17578125 C11.22530853 34.90098465 11.23967957 35.62618805 11.25448608 36.37336731 C11.20886626 41.49335244 9.99115623 44.38403683 6.75 48.3125 C3.0126144 50.8040904 1.17668417 50.77043285 -3.25 50.3125 C-6.00329021 48.51687595 -7.77627461 47.25995078 -9.25 44.3125 C-9.53474008 38.94460889 -9.56375869 33.57565738 -9.60009766 28.20092773 C-9.61665091 26.40554842 -9.6439329 24.61023484 -9.68212891 22.81518555 C-9.73696923 20.22409959 -9.75878148 17.63455732 -9.7734375 15.04296875 C-9.79610687 14.24384567 -9.81877625 13.4447226 -9.84213257 12.62138367 C-9.82575533 8.28890941 -9.65327311 6.79951266 -6.77978516 3.3293457 C-5.94495605 2.66378662 -5.11012695 1.99822754 -4.25 1.3125 C-3.25 0.3125 -3.25 0.3125 0 0 Z'
-
-export function CraftBotPet({
- state,
- progress = 0,
- indeterminate = false,
- completedCount = 0,
- size = 140,
-}: Props) {
- const [wiggling, setWiggling] = useState(false)
- const prevCompleted = useRef(completedCount)
-
- useEffect(() => {
- if (completedCount > prevCompleted.current) {
- setWiggling(true)
- const t = setTimeout(() => setWiggling(false), 600)
- prevCompleted.current = completedCount
- return () => clearTimeout(t)
- }
- prevCompleted.current = completedCount
- }, [completedCount])
-
- const sleeping = state === 'stopped'
- const showBlush = state === 'creating' && progress > 60
- const antennaActive = state !== 'stopped'
- const breatheClass = sleeping ? styles.petSleepBreathe : styles.petBreathe
- const eyeClass = sleeping ? styles.petEyeClosed : styles.petEye
-
- return (
-
-
-
-
-
- )
-}
diff --git a/app/ui_layer/browser/frontend/src/pages/LivingUI/CreationProgress.tsx b/app/ui_layer/browser/frontend/src/pages/LivingUI/CreationProgress.tsx
index 33a233423..59ed11244 100644
--- a/app/ui_layer/browser/frontend/src/pages/LivingUI/CreationProgress.tsx
+++ b/app/ui_layer/browser/frontend/src/pages/LivingUI/CreationProgress.tsx
@@ -2,7 +2,7 @@ import { useMemo } from 'react'
import { Lightbulb } from 'lucide-react'
import type { LivingUITodo } from '../../types'
import { useRotatingHint } from '../../hooks'
-import { CraftBotPet } from './CraftBotPet'
+import { CraftBotMascot } from '@mascot'
import styles from './LivingUIPage.module.css'
interface Props {
@@ -76,10 +76,9 @@ export function CreationProgress({ projectName, todos }: Props) {
return (
-
Creating {projectName}
diff --git a/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.module.css b/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.module.css
index 219249176..26941ffb3 100644
--- a/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.module.css
+++ b/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.module.css
@@ -156,121 +156,6 @@
color: var(--text-muted);
}
-/* CraftBot Pet */
-.petWrapper {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: var(--space-2);
-}
-
-.petFloat {
- display: inline-flex;
- transform-origin: 50% 85%;
-}
-
-.petBreathe {
- transform-origin: 50% 60%;
-}
-
-.petSleepBreathe {
- transform-origin: 50% 60%;
-}
-
-.petEye {
- transform-origin: center;
- transform-box: fill-box;
- animation: petBlink 4.5s infinite;
-}
-
-.petEyeLeft {
- animation-delay: 0ms;
-}
-
-.petEyeRight {
- animation-delay: 120ms;
-}
-
-.petEyeClosed {
- transform-origin: center;
- transform-box: fill-box;
- transform: scaleY(0.08);
-}
-
-.petAntennaPulse {
- transform-origin: center;
- transform-box: fill-box;
-}
-
-.petBlushPulse {
-}
-
-.petWiggle {
-}
-
-/* Body silhouette is near-white — in light mode it blends into the bg,
- so stroke a thin outline. Dark mode needs no outline. */
-.petBody {
- stroke: none;
-}
-
-[data-theme="light"] .petBody {
- stroke: var(--text-muted, var(--border-secondary));
- stroke-width: 3;
- paint-order: stroke fill;
-}
-
-.petSleepZ {
- transform-origin: center;
- transform-box: fill-box;
- animation: petSleepZ 2.4s ease-in-out infinite;
- opacity: 0;
-}
-
-.petSleepZDelayed {
- animation-delay: 1.2s;
-}
-
-@keyframes petFloat {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-6px); }
-}
-
-@keyframes petBreathe {
- 0%, 100% { transform: scale(1); }
- 50% { transform: scale(1.015); }
-}
-
-@keyframes petBlink {
- 0%, 92%, 100% { transform: scaleY(1); }
- 95%, 97% { transform: scaleY(0.05); }
-}
-
-@keyframes petAntennaPulse {
- 0%, 100% { opacity: 0.45; transform: scale(1); }
- 50% { opacity: 1; transform: scale(1.12); }
-}
-
-@keyframes petBlushPulse {
- 0%, 100% { opacity: 0.45; }
- 50% { opacity: 0.9; }
-}
-
-@keyframes petWiggle {
- 0% { transform: translateY(0) rotate(0deg); }
- 20% { transform: translateY(-4px) rotate(-6deg); }
- 45% { transform: translateY(-2px) rotate(6deg); }
- 70% { transform: translateY(-1px) rotate(-3deg); }
- 100% { transform: translateY(0) rotate(0deg); }
-}
-
-@keyframes petSleepZ {
- 0% { opacity: 0; transform: translateY(0); }
- 30% { opacity: 1; }
- 100% { opacity: 0; transform: translateY(-14px); }
-}
-
-
/* Creation Progress */
.creationProgress {
display: flex;
diff --git a/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.tsx b/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.tsx
index 2871a7552..4e5eefa8b 100644
--- a/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.tsx
+++ b/app/ui_layer/browser/frontend/src/pages/LivingUI/LivingUIPage.tsx
@@ -11,7 +11,7 @@ import {
Maximize2,
Minimize2,
} from 'lucide-react'
-import { CraftBotPet } from './CraftBotPet'
+import { CraftBotMascot } from '@mascot'
import { useWebSocket } from '../../contexts/WebSocketContext'
import { useFullscreen } from '../../contexts/FullscreenContext'
import { Button } from '../../components/ui/Button'
@@ -267,7 +267,7 @@ export function LivingUIPage() {
/>
) : project.status === 'launching' ? (
-
+
Launching Living UI...
Installing dependencies, running tests, starting servers
@@ -282,7 +282,7 @@ export function LivingUIPage() {
) : (
-
+
Living UI is not running