Skip to content

Commit fdd8807

Browse files
committed
feat(onboarding): add controller launching step with log streaming
- Remove typing animation components and hooks - Add new launching step to onboarding flow - Implement log streaming for controller initialization - Add launching events to onboarding emitter - Move controller initialization to service with monitoring callback - Add launching view component with spinner and log display
1 parent 10a34c4 commit fdd8807

File tree

11 files changed

+387
-113
lines changed

11 files changed

+387
-113
lines changed

src/cli/tui/app-shell.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { MonitoringCleanup } from "../../agents/monitoring/index.js"
2424
import path from "path"
2525
import { createRequire } from "node:module"
2626
import { resolvePackageJson } from "../../shared/runtime/root.js"
27-
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName, getControllerAgents, initControllerAgent, loadControllerConfig } from "../../shared/workflows/index.js"
27+
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName, getControllerAgents, loadControllerConfig } from "../../shared/workflows/index.js"
2828
import { loadTemplate } from "../../workflows/templates/loader.js"
2929
import { getTemplatePathFromTracking } from "../../shared/workflows/template.js"
3030
import type { TracksConfig, ConditionGroup } from "../../workflows/templates/types"
@@ -198,6 +198,8 @@ export function App(props: { initialToast?: InitialToast }) {
198198
conditionGroups: hasConditionGroups ? template.conditionGroups : undefined,
199199
controllerAgents: needsControllerSelection ? controllers : undefined,
200200
initialProjectName: existingProjectName,
201+
cwd,
202+
cmRoot,
201203
})
202204
setOnboardingService(service)
203205

@@ -268,20 +270,8 @@ export function App(props: { initialToast?: InitialToast }) {
268270
await setSelectedConditions(cmRoot, result.conditions)
269271
}
270272

271-
// Initialize controller agent if selected
272-
if (result.controllerAgentId) {
273-
const agent = controllerAgents()?.find(a => a.id === result.controllerAgentId)
274-
if (agent) {
275-
// Get prompt path from agent config (or use default)
276-
const promptPath = (agent.promptPath as string) || `prompts/agents/${result.controllerAgentId}/system.md`
277-
try {
278-
await initControllerAgent(result.controllerAgentId, promptPath, cwd, cmRoot)
279-
} catch (error) {
280-
console.error("Failed to initialize controller agent:", error)
281-
// Continue anyway - workflow will run without autonomous mode
282-
}
283-
}
284-
}
273+
// Note: Controller initialization is now handled by OnboardingService during the 'launching' step
274+
// The onboard:completed event is only emitted after successful controller initialization
285275

286276
// Start workflow
287277
startWorkflowExecution()
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/** @jsxImportSource @opentui/solid */
2+
/**
3+
* Launching View Component
4+
*
5+
* Shows controller agent initialization progress with spinner and log streaming.
6+
* Streams logs from the agent's log file using the same mechanism as workflow agents.
7+
*/
8+
9+
import { createSignal, onMount, onCleanup, For, Show } from "solid-js"
10+
import { useTheme } from "@tui/shared/context/theme"
11+
import { Spinner } from "@tui/shared/components/spinner"
12+
import { useLogStream } from "../../workflow/hooks/useLogStream"
13+
import { LogLineInline } from "../../workflow/components/shared/log-line"
14+
import type { WorkflowEventBus } from "../../../../../workflows/events/event-bus"
15+
import type { OnboardingService } from "../../../../../workflows/onboarding/service"
16+
17+
export interface LaunchingViewProps {
18+
/** Controller agent name being launched */
19+
controllerName: string
20+
/** Event bus for receiving log messages */
21+
eventBus: WorkflowEventBus
22+
/** Onboarding service to trigger launch */
23+
service: OnboardingService
24+
/** Called when launch fails (optional) */
25+
onError?: (error: string) => void
26+
}
27+
28+
export function LaunchingView(props: LaunchingViewProps) {
29+
const themeCtx = useTheme()
30+
const [status, setStatus] = createSignal<'launching' | 'completed' | 'failed'>('launching')
31+
const [errorMessage, setErrorMessage] = createSignal<string | null>(null)
32+
const [monitoringId, setMonitoringId] = createSignal<number | undefined>(undefined)
33+
34+
// Use the log stream hook to stream logs from the agent's log file
35+
const logStream = useLogStream(() => monitoringId())
36+
37+
onMount(() => {
38+
// Subscribe to monitoring ID event (emitted when agent starts)
39+
const unsubMonitor = props.eventBus.on('onboard:launching_monitor', (event) => {
40+
setMonitoringId(event.monitoringId)
41+
})
42+
43+
// Subscribe to completion event
44+
const unsubCompleted = props.eventBus.on('onboard:launching_completed', () => {
45+
setStatus('completed')
46+
})
47+
48+
// Subscribe to failure event
49+
const unsubFailed = props.eventBus.on('onboard:launching_failed', (event) => {
50+
setStatus('failed')
51+
setErrorMessage(event.error)
52+
props.onError?.(event.error)
53+
})
54+
55+
// Trigger the launch
56+
props.service.launchController()
57+
58+
onCleanup(() => {
59+
unsubMonitor()
60+
unsubCompleted()
61+
unsubFailed()
62+
})
63+
})
64+
65+
const statusIcon = () => {
66+
switch (status()) {
67+
case 'completed':
68+
return '●'
69+
case 'failed':
70+
return '✗'
71+
default:
72+
return null
73+
}
74+
}
75+
76+
const statusColor = () => {
77+
switch (status()) {
78+
case 'completed':
79+
return themeCtx.theme.success
80+
case 'failed':
81+
return themeCtx.theme.error
82+
default:
83+
return themeCtx.theme.primary
84+
}
85+
}
86+
87+
return (
88+
<box flexDirection="column" gap={1}>
89+
{/* Header with spinner/status and controller name */}
90+
<box flexDirection="row" gap={1} alignItems="center">
91+
{status() === 'launching' ? (
92+
<Spinner color={themeCtx.theme.primary} />
93+
) : (
94+
<text fg={statusColor()}>{statusIcon()}</text>
95+
)}
96+
<text fg={themeCtx.theme.text} attributes={1}>
97+
{status() === 'launching'
98+
? `Initializing ${props.controllerName}...`
99+
: status() === 'completed'
100+
? `${props.controllerName} initialized`
101+
: `Failed to initialize ${props.controllerName}`}
102+
</text>
103+
</box>
104+
105+
{/* Thinking indicator */}
106+
<Show when={logStream.latestThinking && status() === 'launching'}>
107+
<box paddingLeft={2}>
108+
<text fg={themeCtx.theme.textMuted}>{logStream.latestThinking}</text>
109+
</box>
110+
</Show>
111+
112+
{/* Log streaming area */}
113+
<Show when={monitoringId() !== undefined}>
114+
<box flexDirection="column" paddingLeft={2} marginTop={1} height={12}>
115+
<Show when={logStream.isConnecting}>
116+
<text fg={themeCtx.theme.textMuted}>Connecting to agent logs...</text>
117+
</Show>
118+
119+
<Show when={logStream.error}>
120+
<text fg={themeCtx.theme.error}>{logStream.error}</text>
121+
</Show>
122+
123+
<Show when={!logStream.isConnecting && !logStream.error && logStream.lines.length > 0}>
124+
<scrollbox
125+
flexGrow={1}
126+
width="100%"
127+
stickyScroll={true}
128+
stickyStart="bottom"
129+
scrollbarOptions={{
130+
showArrows: false,
131+
trackOptions: {
132+
foregroundColor: themeCtx.theme.info,
133+
backgroundColor: themeCtx.theme.borderSubtle,
134+
},
135+
}}
136+
>
137+
<For each={logStream.lines.slice(-15)}>
138+
{(line) => <LogLineInline line={line} />}
139+
</For>
140+
</scrollbox>
141+
</Show>
142+
143+
<Show when={!logStream.isConnecting && !logStream.error && logStream.lines.length === 0}>
144+
<text fg={themeCtx.theme.textMuted}>Waiting for output...</text>
145+
</Show>
146+
</box>
147+
</Show>
148+
149+
{/* Status messages when no monitoring yet */}
150+
<Show when={monitoringId() === undefined && status() === 'launching'}>
151+
<box paddingLeft={2} marginTop={1}>
152+
<text fg={themeCtx.theme.textMuted}>Starting controller agent...</text>
153+
</box>
154+
</Show>
155+
156+
{/* Error message if failed */}
157+
<Show when={status() === 'failed' && errorMessage()}>
158+
<box marginTop={1} paddingLeft={2}>
159+
<text fg={themeCtx.theme.error}>Error: {errorMessage()}</text>
160+
</box>
161+
</Show>
162+
163+
{/* Footer hint */}
164+
<box marginTop={2}>
165+
<text fg={themeCtx.theme.textMuted}>
166+
{status() === 'launching'
167+
? 'Please wait for controller agent to start...'
168+
: status() === 'failed'
169+
? 'Press Escape to go back'
170+
: 'Starting workflow...'}
171+
</text>
172+
</box>
173+
</box>
174+
)
175+
}

src/cli/tui/routes/onboard/components/project-name-input.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import { useTheme } from "@tui/shared/context/theme"
1111
export interface ProjectNameInputProps {
1212
/** Current project name value */
1313
value: Accessor<string>
14-
/** Whether typing animation is complete (shows cursor) */
15-
typingDone: Accessor<boolean>
1614
}
1715

1816
export function ProjectNameInput(props: ProjectNameInputProps) {
@@ -28,7 +26,7 @@ export function ProjectNameInput(props: ProjectNameInputProps) {
2826
minWidth={30}
2927
>
3028
<text fg={themeCtx.theme.text}>
31-
{props.value()}{props.typingDone() ? "_" : ""}
29+
{props.value()}_
3230
</text>
3331
</box>
3432
</box>

src/cli/tui/routes/onboard/components/question-display.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22
/**
33
* Question Display Component
44
*
5-
* Shows PO avatar and typing animation for questions.
5+
* Shows PO avatar and question text.
66
*/
77

88
import type { Accessor } from "solid-js"
99
import { useTheme } from "@tui/shared/context/theme"
1010

1111
export interface QuestionDisplayProps {
12-
/** Currently typed portion of text */
13-
typedText: Accessor<string>
14-
/** Whether typing animation is complete */
15-
typingDone: Accessor<boolean>
12+
/** Question text to display */
13+
question: Accessor<string>
1614
}
1715

1816
export function QuestionDisplay(props: QuestionDisplayProps) {
@@ -32,10 +30,10 @@ export function QuestionDisplay(props: QuestionDisplayProps) {
3230
<text fg={themeCtx.theme.textMuted}>PO</text>
3331
</box>
3432

35-
{/* Typing question */}
33+
{/* Question */}
3634
<box marginBottom={1}>
3735
<text fg={themeCtx.theme.text}>
38-
"{props.typedText()}{props.typingDone() ? "" : "_"}"
36+
"{props.question()}"
3937
</text>
4038
</box>
4139
</>

src/cli/tui/routes/onboard/hooks/use-typing-effect.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/cli/tui/routes/onboard/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,4 @@
55
*/
66

77
export { OnboardView as Onboard, type OnboardViewProps as OnboardProps } from "./onboard-view"
8-
export { useTypingEffect } from "./hooks/use-typing-effect"
98
export { useOnboardKeyboard } from "./hooks/use-onboard-keyboard"

0 commit comments

Comments
 (0)