Skip to content

Commit 7da88fb

Browse files
committed
feat(ui): improve agent character display and typing animation
- Update agent persona from "friendly" to "swagger" with new expressions and phrases - Lock phrase display when activity changes to prevent random reselection - Enhance typing text component with loading dots and proper erase/retry behavior
1 parent ca84f4d commit 7da88fb

4 files changed

Lines changed: 154 additions & 50 deletions

File tree

config/agent-characters.json

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,67 @@
11
{
22
"personas": {
3-
"friendly": {
4-
"baseFace": "(˶ᵔ ᵕ ᵔ˶)",
3+
"swagger": {
4+
"baseFace": "(⌐■_■)",
55
"expressions": {
66
"thinking": "(╭ರ_•́)",
7-
"tool": "(•̀ᴗ•́)و",
7+
"tool": "<(•_•<)",
88
"error": "(╥﹏╥)",
9-
"idle": "(˶ᵔ ᵕ ᵔ˶)"
9+
"idle": "(⌐■_■)"
1010
},
1111
"phrases": {
12-
"thinking": ["Hmm, let me think...", "Processing..."],
13-
"tool": ["On it!", "Working..."],
14-
"error": ["Oops, something went wrong", "Let me try again"],
15-
"idle": ["Ready when you are", "Waiting..."]
12+
"thinking": [
13+
"Aight lemme figure this out real quick",
14+
"Brain.exe is running, one sec",
15+
"Ooh okay I see what you need",
16+
"Processing... not in a robot way tho",
17+
"Gimme a moment, I'm onto something",
18+
"Hmm interesting, let me think on that",
19+
"This is giving me ideas hold up",
20+
"Working on it, trust the process",
21+
"My last two brain cells are on it",
22+
"Cooking up something good rn"
23+
],
24+
"tool": [
25+
"Okay okay I got what I needed from you",
26+
"Perfect, that's exactly what I was looking for",
27+
"Bet, now I can actually do something with this",
28+
"You delivered, now watch me work",
29+
"That's the info I needed, let's go",
30+
"W response, I can work with this",
31+
"Ayyy thanks for that, proceeding now",
32+
"Got it got it, running with it",
33+
"This is what I'm talking about, moving on",
34+
"Locked in, thanks homie"
35+
],
36+
"error": [
37+
"Oof that tool ghosted me, trying plan B",
38+
"Didn't work but I got other tricks",
39+
"Rip that attempt, switching it up",
40+
"Tool said no but I don't take rejection well",
41+
"Minor L, already pivoting tho",
42+
"That one's on the tool not me js",
43+
"Blocked but not stopped, watch this",
44+
"Error schmrror, I got backups",
45+
"Universe said try harder, so I will",
46+
"Speedbump, not a dead end"
47+
],
48+
"idle": [
49+
"Okay your turn, what's next?",
50+
"Ball's in your court homie",
51+
"Ready when you are, no cap",
52+
"Waiting on you, take your time tho",
53+
"What we doing next boss?",
54+
"I'm here, you lead the way",
55+
"Your move chief",
56+
"Standing by for orders",
57+
"Hit me with the next step",
58+
"Listening, what you need?"
59+
]
1660
}
1761
}
1862
},
1963
"agents": {
20-
"cm-workflow-builder": "friendly"
64+
"cm-workflow-builder": "swagger"
2165
},
22-
"defaultPersona": "friendly"
66+
"defaultPersona": "swagger"
2367
}

config/main.agents.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = [
88
// ========================================
99
{
1010
id: 'cm-workflow-builder',
11-
name: 'Ali [Workflow Builder]',
11+
name: 'Ali | The CM Guy',
1212
description: 'CodeMachine workflow builder for creating agents, prompts, and workflows',
1313
promptPath: [
1414
path.join(promptsDir, 'ali', 'ali.md'),

src/cli/tui/routes/workflow/components/output/output-window.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,24 @@ export function OutputWindow(props: OutputWindowProps) {
195195
return "idle"
196196
}
197197

198-
// Get character face and phrase based on agent ID and derived activity
198+
// Get character face based on agent ID and derived activity
199199
const currentFace = () => getFace(displayAgentId() ?? "default", derivedActivity())
200-
const currentPhrase = () => getPhrase(displayAgentId() ?? "default", derivedActivity())
200+
201+
// Lock phrase when activity changes (avoid random re-selection on every render)
202+
const [lockedPhrase, setLockedPhrase] = createSignal("")
203+
let prevActivity: ActivityType | null = null
204+
let prevAgentId: string | null = null
205+
createEffect(() => {
206+
const activity = derivedActivity()
207+
const agentId = displayAgentId() ?? "default"
208+
// Only get new random phrase when activity or agent actually changes
209+
if (activity !== prevActivity || agentId !== prevAgentId) {
210+
prevActivity = activity
211+
prevAgentId = agentId
212+
setLockedPhrase(getPhrase(agentId, activity))
213+
}
214+
})
215+
const currentPhrase = () => lockedPhrase() || getPhrase(displayAgentId() ?? "default", derivedActivity())
201216

202217
// Check if we have something to display (agent or controller in controller view)
203218
const hasDisplayContent = () => props.currentAgent != null || isControllerViewMode()
@@ -325,8 +340,8 @@ export function OutputWindow(props: OutputWindowProps) {
325340

326341
<box flexDirection="row">
327342
<text fg={themeCtx.theme.border}></text>
328-
<Show when={isRunning() && props.latestThinking} fallback={<text fg={themeCtx.theme.textMuted}>{currentPhrase()}</text>}>
329-
<TypingText text={`↳ ${props.latestThinking}`} speed={30} />
343+
<Show when={derivedActivity() !== "idle"} fallback={<text fg={themeCtx.theme.textMuted}>{currentPhrase()}</text>}>
344+
<TypingText text={`↳ ${currentPhrase()}`} speed={30} />
330345
</Show>
331346
</box>
332347
<text fg={themeCtx.theme.border}>╰─</text>

src/cli/tui/routes/workflow/components/output/typing-text.tsx

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
/**
33
* Typing Text Component
44
* Typewriter effect that reveals text character by character
5-
* After 3 seconds, reverse typing effect to disappear
5+
* After typing completes, shows animated loading dots (. → .. → ...)
6+
* When text changes, erases and retypes the new text
67
*/
78

89
import { createSignal, onCleanup, createEffect } from "solid-js"
@@ -11,58 +12,102 @@ import { useTheme } from "@tui/shared/context/theme"
1112
export interface TypingTextProps {
1213
text: string
1314
speed?: number // ms per character
14-
displayDuration?: number // ms to display before disappearing
15+
dotSpeed?: number // ms per dot frame
1516
}
1617

18+
type Phase = "typing" | "dots" | "erasing"
19+
1720
/**
1821
* Typewriter effect component
19-
* Reveals text character by character, then disappears after delay
22+
* Reveals text character by character, then cycles loading dots
23+
* Erases and retypes when text changes
2024
*/
2125
export function TypingText(props: TypingTextProps) {
2226
const themeCtx = useTheme()
2327
const [visibleChars, setVisibleChars] = createSignal(0)
28+
const [dotFrame, setDotFrame] = createSignal(0)
29+
const [phase, setPhase] = createSignal<Phase>("typing")
30+
// displayedText holds the text currently being animated (stable during transitions)
31+
const [displayedText, setDisplayedText] = createSignal(props.text)
32+
// pendingText holds the next text to type after erasing
33+
const [pendingText, setPendingText] = createSignal<string | null>(null)
34+
2435
const speed = () => props.speed ?? 30
25-
const displayDuration = () => props.displayDuration ?? 1000
36+
const dotSpeed = () => props.dotSpeed ?? 400
37+
38+
const DOT_FRAMES = [".", "..", "..."]
2639

40+
// Handle text changes - queue new text and trigger erase
41+
let lastPropsText = props.text
2742
createEffect(() => {
28-
const text = props.text
29-
let eraseInterval: NodeJS.Timeout | undefined
30-
let displayTimeout: NodeJS.Timeout | undefined
43+
const newText = props.text
44+
if (newText !== lastPropsText) {
45+
lastPropsText = newText
46+
const currentDisplayed = displayedText()
47+
if (currentDisplayed !== newText) {
48+
// Queue the new text and start erasing
49+
setPendingText(newText)
50+
setPhase("erasing")
51+
}
52+
}
53+
})
3154

32-
// Reset and start typing when text changes
33-
setVisibleChars(0)
55+
// Main animation loop
56+
createEffect(() => {
57+
const currentPhase = phase()
58+
const text = displayedText()
59+
let interval: NodeJS.Timeout | undefined
3460

35-
// Type in
36-
const typeInterval = setInterval(() => {
37-
setVisibleChars((prev) => {
38-
if (prev >= text.length) {
39-
clearInterval(typeInterval)
40-
// Wait then start erasing
41-
displayTimeout = setTimeout(() => {
42-
eraseInterval = setInterval(() => {
43-
setVisibleChars((prev) => {
44-
if (prev <= 0) {
45-
clearInterval(eraseInterval)
46-
return 0
47-
}
48-
return prev - 1
49-
})
50-
}, speed() / 2) // Erase faster
51-
}, displayDuration())
52-
return prev
53-
}
54-
return prev + 1
55-
})
56-
}, speed())
61+
if (currentPhase === "typing") {
62+
// Type characters one by one
63+
interval = setInterval(() => {
64+
setVisibleChars((prev) => {
65+
if (prev >= text.length) {
66+
clearInterval(interval)
67+
setPhase("dots")
68+
return prev
69+
}
70+
return prev + 1
71+
})
72+
}, speed())
73+
} else if (currentPhase === "dots") {
74+
// Cycle through dot frames
75+
interval = setInterval(() => {
76+
setDotFrame((prev) => (prev + 1) % DOT_FRAMES.length)
77+
}, dotSpeed())
78+
} else if (currentPhase === "erasing") {
79+
// Erase characters quickly
80+
interval = setInterval(() => {
81+
setVisibleChars((prev) => {
82+
if (prev <= 0) {
83+
clearInterval(interval)
84+
// Done erasing - switch to pending text and start typing
85+
const nextText = pendingText()
86+
if (nextText !== null) {
87+
setDisplayedText(nextText)
88+
setPendingText(null)
89+
}
90+
setDotFrame(0)
91+
setPhase("typing")
92+
return 0
93+
}
94+
return prev - 1
95+
})
96+
}, speed() / 2)
97+
}
5798

5899
onCleanup(() => {
59-
if (typeInterval) clearInterval(typeInterval)
60-
if (eraseInterval) clearInterval(eraseInterval)
61-
if (displayTimeout) clearTimeout(displayTimeout)
100+
if (interval) clearInterval(interval)
62101
})
63102
})
64103

65-
const displayText = () => props.text.slice(0, visibleChars())
104+
const displayText = () => {
105+
const baseText = displayedText().slice(0, visibleChars())
106+
if (phase() === "dots") {
107+
return baseText + DOT_FRAMES[dotFrame()]
108+
}
109+
return baseText
110+
}
66111

67112
return <text fg={themeCtx.theme.warning}>{displayText()}</text>
68113
}

0 commit comments

Comments
 (0)