11import { useState , useEffect , useMemo , useRef , useCallback } from 'react'
2- import { createTraverseClient } from './client/traverseClient'
3- import { useExecution } from './hooks/useExecution '
2+ import { createTraverseClient , type TraceEvent } from './client/traverseClient'
3+ import { useAppState } from './hooks/useAppState '
44import { parseOutput } from './client/traverseOutput'
55
66const BASE_URL =
77 import . meta. env . VITE_TRAVERSE_BASE_URL ??
88 import . meta. env . VITE_TRAVERSE_RUNTIME_URL ??
99 'http://127.0.0.1:8787'
1010const WORKSPACE = import . meta. env . VITE_TRAVERSE_WORKSPACE ?? 'local-default'
11- const CAPABILITY_ID = import . meta. env . VITE_TRAVERSE_CAPABILITY_ID ?? 'traverse-starter.process '
11+ const APP_ID = import . meta. env . VITE_TRAVERSE_APP_ID ?? 'traverse-starter'
1212const NOTE_MAX_LENGTH = 2000
1313
1414function App ( ) {
1515 const [ note , setNote ] = useState ( '' )
1616 const [ runtimeStatus , setRuntimeStatus ] = useState < 'checking' | 'online' | 'offline' > ( 'checking' )
17+ const [ submitting , setSubmitting ] = useState ( false )
18+ const [ submitError , setSubmitError ] = useState < string | null > ( null )
19+ const [ trace , setTrace ] = useState < TraceEvent [ ] > ( [ ] )
1720 const formRef = useRef < HTMLFormElement > ( null )
1821
1922 const client = useMemo ( ( ) => createTraverseClient ( BASE_URL ) , [ ] )
20- const { state, run } = useExecution ( client , WORKSPACE )
23+ const { state, output , error , executionId } = useAppState ( client , WORKSPACE , APP_ID )
2124
2225 useEffect ( ( ) => {
2326 let active = true
@@ -34,26 +37,51 @@ function App() {
3437 return ( ) => { active = false ; clearInterval ( interval ) }
3538 } , [ ] )
3639
37- const isRunning = state . phase === 'loading' || state . phase === 'polling'
40+ useEffect ( ( ) => {
41+ if ( state !== 'results' || ! executionId ) {
42+ return
43+ }
44+ let active = true
45+ client . fetchTrace ( WORKSPACE , executionId ) . then (
46+ ( events ) => { if ( active ) setTrace ( events ) } ,
47+ ( ) => { if ( active ) setTrace ( [ ] ) } ,
48+ )
49+ return ( ) => { active = false }
50+ } , [ client , state , executionId ] )
51+
52+ const isRunning = state === 'processing' || submitting
3853 const canSubmit = note . trim ( ) . length > 0 && runtimeStatus === 'online' && ! isRunning
3954
40- const handleStartWorkflow = useCallback ( ( e ?: React . FormEvent ) => {
55+ const handleStartWorkflow = useCallback ( async ( e ?: React . FormEvent ) => {
4156 e ?. preventDefault ( )
4257 if ( ! canSubmit ) return
43- run ( CAPABILITY_ID , { note } )
44- } , [ canSubmit , run , note ] )
58+ setSubmitting ( true )
59+ setSubmitError ( null )
60+ setTrace ( [ ] )
61+ try {
62+ // Omit session_id so each submit starts a new runtime session at initial_state.
63+ await client . sendCommand ( WORKSPACE , APP_ID , 'submit' , { note } )
64+ } catch ( err ) {
65+ setSubmitError ( String ( err ) )
66+ } finally {
67+ setSubmitting ( false )
68+ }
69+ } , [ canSubmit , client , note ] )
4570
4671 const handleNoteKeyDown = ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
4772 if ( ( e . metaKey || e . ctrlKey ) && e . key === 'Enter' ) {
4873 e . preventDefault ( )
49- handleStartWorkflow ( )
74+ void handleStartWorkflow ( )
5075 }
5176 }
5277
5378 const handleNoteChange = ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
5479 setNote ( e . target . value . slice ( 0 , NOTE_MAX_LENGTH ) )
5580 }
5681
82+ const displayError = submitError ?? ( state === 'error' ? error : null )
83+ const parsed = state === 'results' ? parseOutput ( output ) : null
84+
5785 return (
5886 < div style = { { maxWidth : '800px' , margin : '40px auto' , padding : '0 20px' } } >
5987 < header style = { { marginBottom : '40px' , textAlign : 'center' } } >
@@ -110,15 +138,15 @@ function App() {
110138 </ div >
111139 </ div >
112140 < div style = { { marginTop : '20px' , paddingTop : '16px' , borderTop : '1px solid rgba(255,255,255,0.05)' , fontSize : '0.85rem' , color : 'var(--text-muted)' } } >
113- Workspace: < strong > { WORKSPACE } </ strong > · Capability : < strong > { CAPABILITY_ID } </ strong >
141+ Workspace: < strong > { WORKSPACE } </ strong > · App : < strong > { APP_ID } </ strong >
114142 </ div >
115143 </ section >
116144
117145 < section className = "glass-panel" style = { { padding : '24px' } } >
118146 < h2 style = { { fontSize : '1.25rem' , marginBottom : '16px' , fontWeight : 600 } } >
119147 Start Workflow
120148 </ h2 >
121- < form ref = { formRef } onSubmit = { handleStartWorkflow } >
149+ < form ref = { formRef } onSubmit = { ( e ) => { void handleStartWorkflow ( e ) } } >
122150 < div style = { { marginBottom : '20px' } } >
123151 < label htmlFor = "note-input" style = { { display : 'block' , fontSize : '0.85rem' , color : 'var(--text-muted)' , marginBottom : '8px' , fontWeight : 500 } } >
124152 Starter Input Note
@@ -170,78 +198,73 @@ function App() {
170198 Execution Output
171199 </ h2 >
172200
173- { runtimeStatus === 'offline' && state . phase === 'idle' && (
201+ { runtimeStatus === 'offline' && state === 'idle' && ! displayError && (
174202 < div style = { { color : 'var(--text-muted)' , fontSize : '0.95rem' , textAlign : 'center' , paddingTop : '32px' } } >
175203 Connect to the Traverse runtime to see workflow output here.
176204 </ div >
177205 ) }
178206
179- { runtimeStatus !== 'offline' && state . phase === 'idle' && (
207+ { runtimeStatus !== 'offline' && state === 'idle' && ! displayError && ! submitting && (
180208 < div style = { { color : 'var(--text-muted)' , fontSize : '0.95rem' , textAlign : 'center' , paddingTop : '32px' } } >
181209 Submit a note above to start a workflow.
182210 </ div >
183211 ) }
184212
185- { state . phase === 'loading' && (
186- < div style = { { color : 'var(--text-secondary)' } } > Starting execution…</ div >
187- ) }
188-
189- { state . phase === 'polling' && (
213+ { ( state === 'processing' || submitting ) && ! displayError && (
190214 < div style = { { color : 'var(--text-secondary)' } } >
191- Polling execution < code > { state . executionId } </ code > …
215+ { state === 'processing' ? 'Processing…' : 'Submitting command…' }
192216 </ div >
193217 ) }
194218
195- { state . phase === 'failed' && (
219+ { displayError && (
196220 < div style = { { color : '#ef4444' } } >
197- < strong > Error:</ strong > { state . error }
221+ < strong > Error:</ strong > { displayError }
198222 </ div >
199223 ) }
200224
201- { state . phase === 'succeeded' && ( ( ) => {
202- const output = parseOutput ( state . result . output )
203- return output ? (
204- < div style = { { display : 'flex' , flexDirection : 'column' , gap : '16px' } } >
205- < OutputField label = "Title" value = { output . title } />
206- < OutputField label = "Note Type" value = { output . noteType } />
207- < OutputField label = "Status" value = { output . status } />
208- < OutputField label = "Suggested Next Action" value = { output . suggestedNextAction } />
225+ { state === 'results' && parsed && (
226+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : '16px' } } >
227+ < OutputField label = "Title" value = { parsed . title } />
228+ < OutputField label = "Note Type" value = { parsed . noteType } />
229+ < OutputField label = "Status" value = { parsed . status } />
230+ < OutputField label = "Suggested Next Action" value = { parsed . suggestedNextAction } />
231+ < div >
232+ < div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '6px' } } > Tags</ div >
233+ < div style = { { display : 'flex' , gap : '6px' , flexWrap : 'wrap' } } >
234+ { parsed . tags . map ( tag => (
235+ < span key = { tag } style = { { padding : '2px 10px' , background : 'rgba(139,92,246,0.15)' , color : 'var(--color-accent)' , borderRadius : '20px' , fontSize : '0.85rem' } } >
236+ { tag }
237+ </ span >
238+ ) ) }
239+ </ div >
240+ </ div >
241+ { trace . length > 0 && (
209242 < div >
210- < div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '6px' } } > Tags</ div >
211- < div style = { { display : 'flex' , gap : '6px' , flexWrap : 'wrap' } } >
212- { output . tags . map ( tag => (
213- < span key = { tag } style = { { padding : '2px 10px' , background : 'rgba(139,92,246,0.15)' , color : 'var(--color-accent)' , borderRadius : '20px' , fontSize : '0.85rem' } } >
214- { tag }
215- </ span >
216- ) ) }
243+ < div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '8px' } } >
244+ Trace Events ({ trace . length } )
217245 </ div >
246+ < ul style = { { listStyle : 'none' , padding : 0 , margin : 0 , display : 'flex' , flexDirection : 'column' , gap : '8px' } } >
247+ { trace . map ( ( event , index ) => (
248+ < li key = { `${ event . timestamp } -${ index } ` } style = { { fontSize : '0.85rem' , color : 'var(--text-secondary)' , background : 'rgba(0,0,0,0.25)' , padding : '8px 12px' , borderRadius : '6px' } } >
249+ < div > < strong > { event . event_type } </ strong > · { event . timestamp } </ div >
250+ { event . data !== undefined && (
251+ < pre style = { { marginTop : '6px' , fontSize : '0.8rem' , color : 'var(--text-muted)' , overflow : 'auto' } } >
252+ { JSON . stringify ( event . data , null , 2 ) }
253+ </ pre >
254+ ) }
255+ </ li >
256+ ) ) }
257+ </ ul >
218258 </ div >
219- { state . trace . length > 0 && (
220- < div >
221- < div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '8px' } } >
222- Trace Events ({ state . trace . length } )
223- </ div >
224- < ul style = { { listStyle : 'none' , padding : 0 , margin : 0 , display : 'flex' , flexDirection : 'column' , gap : '8px' } } >
225- { state . trace . map ( ( event , index ) => (
226- < li key = { `${ event . timestamp } -${ index } ` } style = { { fontSize : '0.85rem' , color : 'var(--text-secondary)' , background : 'rgba(0,0,0,0.25)' , padding : '8px 12px' , borderRadius : '6px' } } >
227- < div > < strong > { event . event_type } </ strong > · { event . timestamp } </ div >
228- { event . data !== undefined && (
229- < pre style = { { marginTop : '6px' , fontSize : '0.8rem' , color : 'var(--text-muted)' , overflow : 'auto' } } >
230- { JSON . stringify ( event . data , null , 2 ) }
231- </ pre >
232- ) }
233- </ li >
234- ) ) }
235- </ ul >
236- </ div >
237- ) }
238- </ div >
239- ) : (
240- < pre style = { { background : 'rgba(0,0,0,0.4)' , padding : '16px' , borderRadius : '6px' , fontSize : '0.85rem' , color : 'var(--text-primary)' , overflow : 'auto' } } >
241- { JSON . stringify ( state . result . output , null , 2 ) }
242- </ pre >
243- )
244- } ) ( ) }
259+ ) }
260+ </ div >
261+ ) }
262+
263+ { state === 'results' && ! parsed && output != null && (
264+ < pre style = { { background : 'rgba(0,0,0,0.4)' , padding : '16px' , borderRadius : '6px' , fontSize : '0.85rem' , color : 'var(--text-primary)' , overflow : 'auto' } } >
265+ { JSON . stringify ( output , null , 2 ) }
266+ </ pre >
267+ ) }
245268 </ section >
246269
247270 </ div >
0 commit comments