Skip to content

Commit fdc9a6e

Browse files
authored
Merge pull request #73 from moazbuilds/feat/ascii-faces
Feat/ascii faces for agents
2 parents 762c558 + fff11cb commit fdc9a6e

11 files changed

Lines changed: 1055 additions & 71 deletions

File tree

config/agent-characters.json

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
{
2+
"personas": {
3+
"friendly": {
4+
"baseFace": "(˶ᵔ ᵕ ᵔ˶)",
5+
"expressions": {
6+
"thinking": "(╭ರ_•́)",
7+
"tool": "(•̀ᴗ•́)و",
8+
"error": "(╥﹏╥)",
9+
"idle": "(˶ᵔ ᵕ ᵔ˶)"
10+
},
11+
"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..."]
16+
}
17+
},
18+
"analytical": {
19+
"baseFace": "[•_•]",
20+
"expressions": {
21+
"thinking": "[•_•]~",
22+
"tool": "[◉_◉]",
23+
"error": "[x_x]",
24+
"idle": "[•_•]"
25+
},
26+
"phrases": {
27+
"thinking": ["Analyzing...", "Computing..."],
28+
"tool": ["Executing...", "Running..."],
29+
"error": ["Error encountered", "Retrying..."],
30+
"idle": ["Standing by", "Awaiting input"]
31+
}
32+
},
33+
"cheerful": {
34+
"baseFace": "◕‿◕",
35+
"expressions": {
36+
"thinking": "◕~◕",
37+
"tool": "◕!◕",
38+
"error": "◕_◕",
39+
"idle": "◕‿◕"
40+
},
41+
"phrases": {
42+
"thinking": ["Pondering...", "Considering..."],
43+
"tool": ["Let me do that!", "Working on it..."],
44+
"error": ["Hmm, that didn't work", "Let me try again"],
45+
"idle": ["Here to help!", "Ready for action"]
46+
}
47+
},
48+
"technical": {
49+
"baseFace": "{•_•}",
50+
"expressions": {
51+
"thinking": "{•~•}",
52+
"tool": "{•!•}",
53+
"error": "{•x•}",
54+
"idle": "{•_•}"
55+
},
56+
"phrases": {
57+
"thinking": ["Processing...", "Analyzing..."],
58+
"tool": ["Executing...", "Working..."],
59+
"error": ["Error", "Retrying..."],
60+
"idle": ["Ready", "Waiting..."]
61+
}
62+
},
63+
"precise": {
64+
"baseFace": "<•_•>",
65+
"expressions": {
66+
"thinking": "<•~•>",
67+
"tool": "<•!•>",
68+
"error": "<•x•>",
69+
"idle": "<•_•>"
70+
},
71+
"phrases": {
72+
"thinking": ["Evaluating...", "Checking..."],
73+
"tool": ["Validating...", "Running checks..."],
74+
"error": ["Issue detected", "Reviewing..."],
75+
"idle": ["Ready", "Monitoring..."]
76+
}
77+
}
78+
},
79+
"agents": {
80+
"bmad-analyst": "friendly",
81+
"bmad-pm": "analytical",
82+
"bmad-ux": "cheerful",
83+
"bmad-architect": "analytical",
84+
"bmad-epics": "analytical",
85+
"bmad-tea": "precise",
86+
"bmad-readiness": "precise",
87+
"bmad-sprints": "cheerful",
88+
"bmad-stories": "cheerful",
89+
"bmad-dev": "technical",
90+
"bmad-po": "analytical",
91+
"cm-workflow-builder": "friendly",
92+
"init": "friendly",
93+
"principal-analyst": "analytical",
94+
"blueprint-orchestrator": "analytical",
95+
"plan-agent": "analytical",
96+
"task-breakdown": "technical",
97+
"context-manager": "technical",
98+
"code-generation": "technical",
99+
"task-sanity-check": "precise",
100+
"runtime-prep": "technical",
101+
"git-commit": "technical",
102+
"plan-fallback": "analytical"
103+
},
104+
"defaultPersona": "friendly"
105+
}

src/cli/tui/routes/workflow/components/modals/log-viewer/index.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,27 @@ export interface LogViewerProps {
2222
export function LogViewer(props: LogViewerProps) {
2323
const dimensions = useTerminalDimensions()
2424

25-
const monitoringId = () => props.getMonitoringId(props.agentId)
26-
const logStream = useLogStream(monitoringId)
27-
2825
const visibleLines = createMemo(() => {
2926
const height = dimensions()?.height ?? 40
3027
return Math.max(5, height - 9)
3128
})
3229

33-
// Close on Escape
30+
const logStream = useLogStream({
31+
monitoringAgentId: () => props.getMonitoringId(props.agentId),
32+
visibleLineCount: visibleLines
33+
})
34+
35+
// Handle keyboard navigation
36+
const handleLoadEarlier = () => {
37+
if (logStream.hasMoreAbove) {
38+
logStream.loadEarlierLines()
39+
}
40+
}
41+
3442
useModalKeyboard({
3543
onClose: props.onClose,
44+
onGoTop: handleLoadEarlier, // g key loads earlier lines
45+
onPageUp: handleLoadEarlier, // PageUp also loads earlier lines
3646
})
3747

3848
return (
@@ -50,9 +60,16 @@ export function LogViewer(props: LogViewerProps) {
5060
error={logStream.error}
5161
visibleHeight={visibleLines()}
5262
isRunning={logStream.isRunning}
63+
totalLineCount={logStream.totalLineCount}
64+
hasMoreAbove={logStream.hasMoreAbove}
65+
isLoadingEarlier={logStream.isLoadingEarlier}
66+
loadEarlierError={logStream.loadEarlierError}
67+
onLoadMore={() => logStream.loadEarlierLines()}
68+
onPauseTrimmingChange={(paused) => logStream.setPauseTrimming(paused)}
5369
/>
5470
<LogFooter
55-
total={logStream.lines.length}
71+
total={logStream.totalLineCount}
72+
hasMoreAbove={logStream.hasMoreAbove}
5673
isRunning={logStream.isRunning}
5774
/>
5875
</box>

src/cli/tui/routes/workflow/components/modals/log-viewer/log-content.tsx

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* Displays scrollable log lines with OpenTUI scrollbox.
66
*/
77

8-
import { Show, For } from "solid-js"
8+
import type { ScrollBoxRenderable } from "@opentui/core"
9+
import { Show, For, createSignal, createEffect, onCleanup, on } from "solid-js"
910
import { useTheme } from "@tui/shared/context/theme"
1011
import { LogLine } from "../../shared/log-line"
1112
import { LogTable } from "../../shared/log-table"
1213
import { groupLinesWithTables } from "../../shared/markdown-table"
14+
import { debug } from "../../../../../../../shared/logging/logger.js"
1315

1416
export interface LogContentProps {
1517
lines: string[]
@@ -18,10 +20,88 @@ export interface LogContentProps {
1820
error: string | null
1921
visibleHeight: number
2022
isRunning?: boolean
23+
totalLineCount?: number
24+
hasMoreAbove?: boolean
25+
isLoadingEarlier?: boolean
26+
loadEarlierError?: string | null
27+
onLoadMore?: () => number
28+
onPauseTrimmingChange?: (paused: boolean) => void
2129
}
2230

2331
export function LogContent(props: LogContentProps) {
32+
debug('[LogContent] Component rendering, lines=%d, isLoading=%s, hasMoreAbove=%s',
33+
props.lines.length, props.isLoading, props.hasMoreAbove)
34+
2435
const themeCtx = useTheme()
36+
const [scrollRef, setScrollRef] = createSignal<ScrollBoxRenderable | undefined>()
37+
const [userScrolledAway, setUserScrolledAway] = createSignal(false)
38+
39+
// Reset userScrolledAway when lines reset (indicates agent/log change)
40+
// Use on() to explicitly track only lines.length, preventing unintended effect re-runs
41+
createEffect(
42+
on(
43+
() => props.lines.length,
44+
(currentCount, prev) => {
45+
// If lines dropped significantly (more than 50% or to near zero), it's a new log
46+
if (prev !== undefined && prev > 10 && currentCount < prev * 0.5) {
47+
debug('[LogContent] Lines reset detected (prev=%d, current=%d), resetting scroll state', prev, currentCount)
48+
setUserScrolledAway(false)
49+
}
50+
}
51+
)
52+
)
53+
54+
// Handle scroll events: load earlier lines + track if user scrolled away from bottom
55+
// Use on() to only track scrollRef changes, not other props
56+
createEffect(
57+
on(scrollRef, (ref) => {
58+
debug('[LogContent] Effect running, ref exists: %s', !!ref)
59+
if (!ref) return
60+
61+
const handleScrollChange = () => {
62+
const scrollTop = ref.scrollTop
63+
const scrollHeight = ref.scrollHeight
64+
const viewportHeight = ref.height
65+
const maxScroll = Math.max(0, scrollHeight - viewportHeight)
66+
const isAtBottom = scrollTop >= maxScroll - 3
67+
68+
debug('[LogContent] scroll: top=%d, max=%d, atBottom=%s, hasMore=%s', scrollTop, maxScroll, isAtBottom, props.hasMoreAbove)
69+
70+
// Track if user scrolled away from bottom (to disable stickyScroll)
71+
if (!isAtBottom && !userScrolledAway()) {
72+
debug('[LogContent] User scrolled away from bottom')
73+
setUserScrolledAway(true)
74+
} else if (isAtBottom && userScrolledAway()) {
75+
debug('[LogContent] User returned to bottom')
76+
setUserScrolledAway(false)
77+
}
78+
79+
// Trigger load when near the top (within 3 lines) - skip if already loading
80+
if (scrollTop <= 3 && props.hasMoreAbove && props.onLoadMore && !props.isLoadingEarlier) {
81+
debug('[LogContent] Loading earlier lines...')
82+
const linesLoaded = props.onLoadMore()
83+
debug('[LogContent] Lines loaded: %d', linesLoaded)
84+
if (linesLoaded > 0) {
85+
ref.scrollTop = linesLoaded // Maintain view position
86+
}
87+
}
88+
}
89+
90+
debug('[LogContent] Setting up scroll listener, verticalScrollBar exists: %s', !!ref.verticalScrollBar)
91+
ref.verticalScrollBar?.on("change", handleScrollChange)
92+
onCleanup(() => ref.verticalScrollBar?.off("change", handleScrollChange))
93+
})
94+
)
95+
96+
// Compute whether stickyScroll should be active (only when running AND user hasn't scrolled away)
97+
const shouldStickyScroll = () => (props.isRunning ?? true) && !userScrolledAway()
98+
99+
// Notify parent when user scrolls away (to pause trimming in log stream)
100+
createEffect(
101+
on(userScrolledAway, (scrolledAway) => {
102+
props.onPauseTrimmingChange?.(scrolledAway)
103+
})
104+
)
25105

26106
return (
27107
<box flexGrow={1} flexDirection="column" paddingLeft={1} paddingRight={1} paddingTop={1}>
@@ -52,10 +132,19 @@ export function LogContent(props: LogContentProps) {
52132
</box>
53133
}
54134
>
135+
{/* Loading earlier lines indicator */}
136+
<Show when={props.isLoadingEarlier}>
137+
<text fg={themeCtx.theme.info}>↑ Loading earlier lines...</text>
138+
</Show>
139+
{/* Error loading earlier lines */}
140+
<Show when={props.loadEarlierError}>
141+
<text fg={themeCtx.theme.error}>↑ Error: {props.loadEarlierError}</text>
142+
</Show>
55143
<scrollbox
144+
ref={(r: ScrollBoxRenderable) => setScrollRef(r)}
56145
height={props.visibleHeight}
57146
width="100%"
58-
stickyScroll={props.isRunning ?? true}
147+
stickyScroll={shouldStickyScroll()}
59148
stickyStart="bottom"
60149
scrollbarOptions={{
61150
showArrows: true,

src/cli/tui/routes/workflow/components/modals/log-viewer/log-footer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useTheme } from "@tui/shared/context/theme"
1111
export interface LogFooterProps {
1212
total: number
1313
isRunning: boolean
14+
hasMoreAbove?: boolean
1415
}
1516

1617
export function LogFooter(props: LogFooterProps) {
@@ -20,17 +21,21 @@ export function LogFooter(props: LogFooterProps) {
2021
<>
2122
<Show when={props.total > 0}>
2223
<box paddingLeft={1} paddingRight={1} flexDirection="row">
24+
<Show when={props.hasMoreAbove}>
25+
<text fg={themeCtx.theme.textMuted}>... </text>
26+
</Show>
2327
<text fg={themeCtx.theme.textMuted}>
2428
{props.total} lines
2529
</text>
2630
<Show when={props.isRunning}>
27-
<text fg={themeCtx.theme.warning}> • Live updates enabled</text>
31+
<text fg={themeCtx.theme.warning}> • Live</text>
2832
</Show>
2933
</box>
3034
</Show>
3135
<box paddingLeft={1} paddingRight={1}>
3236
<text fg={themeCtx.theme.textMuted}>
3337
[Esc] Close [Up/Down] Scroll
38+
<Show when={props.hasMoreAbove}> [g] Load earlier</Show>
3439
</text>
3540
</box>
3641
</>

0 commit comments

Comments
 (0)