Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/delegation-tree-interface-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@perstack/tui-components": patch
---

Add delegation tree to InterfacePanel for real-time expert visualization
7 changes: 5 additions & 2 deletions packages/tui-components/src/execution/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ export const ExecutionApp = (props: ExecutionAppProps) => {
}}
</Static>
<InterfacePanel
runtimeInfo={state.runtimeInfo}
query={state.query}
runStatus={state.runStatus}
streaming={state.streaming}
onSubmit={state.handleSubmit}
delegationTreeState={state.delegationTreeState}
inProgressCount={state.inProgressCount}
formattedTotalTokens={state.formattedTotalTokens}
elapsedTime={state.elapsedTime}
/>
</Box>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ActivityLogItem = ({ activity }: ActivityLogItemProps): React.React

if (delegatedBy) {
return (
<Box marginLeft={1} flexDirection="column">
<Box flexDirection="column">
<Text dimColor bold>
[{expertKey}]
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Text } from "ink"
import type React from "react"
import { colors } from "../../colors.js"
import { USAGE_INDICATORS } from "../../constants.js"
import type { DelegationTreeState, FlatTreeNode } from "../hooks/use-delegation-tree.js"
import { flattenTree } from "../hooks/use-delegation-tree.js"
import { useSpinner } from "../hooks/use-spinner.js"

function getUsageIcon(percent: number): string {
if (percent <= 25) return USAGE_INDICATORS.LOW
if (percent <= 50) return USAGE_INDICATORS.MEDIUM
if (percent <= 75) return USAGE_INDICATORS.HIGH
return USAGE_INDICATORS.FULL
}

function buildPrefix(flatNode: FlatTreeNode): string {
if (flatNode.depth === 0) return ""

let prefix = ""
for (let i = 1; i < flatNode.ancestorIsLast.length; i++) {
prefix += flatNode.ancestorIsLast[i] ? " " : "│ "
}
prefix += flatNode.isLast ? "└ " : "├ "
return prefix
}

function TreeNodeLine({
flatNode,
spinner,
}: {
flatNode: FlatTreeNode
spinner: string
}): React.ReactNode {
const { node } = flatNode
const prefix = buildPrefix(flatNode)

let indicator: React.ReactNode
switch (node.status) {
case "running":
indicator = <Text color={colors.accent}>{spinner}</Text>
break
case "suspending":
indicator = <Text color={colors.muted}>⏸</Text>
break
case "completed":
indicator = <Text color={colors.success}>✓</Text>
break
case "error":
indicator = <Text color={colors.destructive}>✗</Text>
break
}

const usagePercent = (node.contextWindowUsage * 100).toFixed(1)
const usageIcon = getUsageIcon(node.contextWindowUsage * 100)
const showUsage = node.status !== "completed"

return (
<Text>
<Text dimColor>{prefix}</Text>
{indicator}
<Text> </Text>
<Text bold>{node.expertName}</Text>
<Text dimColor>: </Text>
<Text>{node.actionLabel}</Text>
{node.actionFileArg ? <Text color={colors.muted}> {node.actionFileArg}</Text> : null}
{showUsage ? (
<>
<Text dimColor> · </Text>
<Text>
{usageIcon} {usagePercent}%
</Text>
</>
) : null}
</Text>
)
}

type DelegationTreeProps = {
state: DelegationTreeState
}

export const DelegationTree = ({ state }: DelegationTreeProps): React.ReactNode => {
const flatNodes = flattenTree(state)
const hasRunning = flatNodes.some(
(n) => n.node.status === "running" || n.node.status === "suspending",
)
const spinner = useSpinner({ isActive: hasRunning })

if (flatNodes.length === 0) return null

return (
<>
{flatNodes.map((flatNode) => (
<TreeNodeLine key={flatNode.node.runId} flatNode={flatNode} spinner={spinner} />
))}
</>
)
}
1 change: 1 addition & 0 deletions packages/tui-components/src/execution/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ActivityLogItem } from "./activity-log-panel.js"
export { DelegationTree } from "./delegation-tree.js"
export { InterfacePanel } from "./interface-panel.js"
Original file line number Diff line number Diff line change
@@ -1,64 +1,38 @@
import type { StreamingState } from "@perstack/react"
import { Box, Text, useInput } from "ink"
import type React from "react"
import { colors } from "../../colors.js"
import { USAGE_INDICATORS } from "../../constants.js"
import { useTextInput } from "../../hooks/use-text-input.js"
import type { RuntimeInfo } from "../../types/index.js"
import type { DelegationTreeState } from "../hooks/use-delegation-tree.js"
import type { RunStatus } from "../hooks/use-execution-state.js"
import { useSpinner } from "../hooks/use-spinner.js"
import { useStreamingPhase } from "../hooks/use-streaming-phase.js"
import { DelegationTree } from "./delegation-tree.js"

type InterfacePanelProps = {
runtimeInfo: RuntimeInfo
query: string | undefined
runStatus: RunStatus
streaming: StreamingState
onSubmit: (query: string) => void
}

function getUsageIcon(percent: number): string {
if (percent <= 25) return USAGE_INDICATORS.LOW
if (percent <= 50) return USAGE_INDICATORS.MEDIUM
if (percent <= 75) return USAGE_INDICATORS.HIGH
return USAGE_INDICATORS.FULL
delegationTreeState: DelegationTreeState
inProgressCount: number
formattedTotalTokens: string
elapsedTime: string
}

export const InterfacePanel = ({
runtimeInfo,
query,
runStatus,
streaming,
onSubmit,
delegationTreeState,
inProgressCount,
formattedTotalTokens,
elapsedTime,
}: InterfacePanelProps): React.ReactNode => {
const streamingPhase = useStreamingPhase(streaming)
const isSpinnerActive = runStatus === "running"
const spinner = useSpinner({ isActive: isSpinnerActive })

const { input, handleInput } = useTextInput({
onSubmit,
canSubmit: runStatus !== "running",
})

useInput(handleInput)

// Derive status label
let statusLabel: React.ReactNode
if (runStatus === "waiting") {
statusLabel = <Text color={colors.accent}>Waiting for query...</Text>
} else if (runStatus === "completed") {
statusLabel = <Text color={colors.success}>Completed</Text>
} else if (runStatus === "stopped") {
statusLabel = <Text color={colors.warn}>Stopped</Text>
} else if (streamingPhase === "reasoning") {
statusLabel = <Text>Streaming Reasoning...</Text>
} else if (streamingPhase === "generating") {
statusLabel = <Text>Streaming Generation...</Text>
} else {
statusLabel = <Text>Running...</Text>
}

const step = runtimeInfo.currentStep !== undefined ? String(runtimeInfo.currentStep) : "–"
const usagePercent = (runtimeInfo.contextWindowUsage * 100).toFixed(1)
const usageIcon = getUsageIcon(runtimeInfo.contextWindowUsage * 100)
const isWaiting = runStatus === "waiting"

return (
<Box
Expand All @@ -70,16 +44,24 @@ export const InterfacePanel = ({
borderLeft={false}
borderRight={false}
>
<Text>
{spinner ? <Text color={colors.accent}>{spinner} </Text> : null}
{statusLabel}
<Text dimColor> ┃ </Text>
<Text dimColor>↻ </Text>
<Text>{step}</Text>
<Text dimColor> ┃ </Text>
<Text>{usageIcon} </Text>
<Text>{usagePercent}%</Text>
</Text>
{isWaiting ? (
<Text color={colors.accent}>Waiting for query...</Text>
) : (
<>
<Text color={colors.success} wrap="truncate">
<Text bold>Query: </Text>
<Text>{query || "–"}</Text>
</Text>
<Text dimColor>
<Text>{inProgressCount} in progress experts</Text>
<Text> · </Text>
<Text>{elapsedTime}</Text>
<Text> · </Text>
<Text>{formattedTotalTokens} tokens</Text>
</Text>
</>
)}
<DelegationTree state={delegationTreeState} />
<Text>
<Text color={colors.muted}>&gt; </Text>
<Text>{input}</Text>
Expand Down
9 changes: 8 additions & 1 deletion packages/tui-components/src/execution/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export {
type DelegationTreeNode,
type DelegationTreeState,
type ExpertRunStatus,
type FlatTreeNode,
useDelegationTree,
} from "./use-delegation-tree.js"
export { useElapsedTime } from "./use-elapsed-time.js"
export {
type ExecutionState,
type LogEntry,
Expand All @@ -6,4 +14,3 @@ export {
useExecutionState,
} from "./use-execution-state.js"
export { useSpinner } from "./use-spinner.js"
export { type StreamingPhase, useStreamingPhase } from "./use-streaming-phase.js"
Loading