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/log-viewer-tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@perstack/tui-components": patch
---

Add interactive log viewer TUI with delegation tree, run list, event/checkpoint drill-down screens
5 changes: 3 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/tui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"dependencies": {
"@perstack/core": "workspace:*",
"@perstack/log": "workspace:*",
"@perstack/react": "workspace:*",
"ink": "^6.7.0",
"react": "^19.2.4"
Expand Down
55 changes: 55 additions & 0 deletions packages/tui-components/src/components/bottom-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Box, Text, useInput } from "ink"
import type React from "react"
import { colors } from "../colors.js"
import { useTextInput } from "../hooks/use-text-input.js"

type BottomPanelProps = {
children: React.ReactNode
onSubmit: (query: string) => void
canSubmit?: boolean
inputPlaceholder?: string
}

export const BottomPanel = ({
children,
onSubmit,
canSubmit = true,
inputPlaceholder,
}: BottomPanelProps) => {
const { input, handleInput } = useTextInput({
onSubmit,
canSubmit,
})

useInput(handleInput)

return (
<Box
flexDirection="column"
borderStyle="single"
borderColor={colors.muted}
borderTop={true}
borderBottom={false}
borderLeft={false}
borderRight={false}
>
{children}
<Text>
<Text color={colors.muted}>&gt; </Text>
{input ? (
<>
<Text>{input}</Text>
<Text color={colors.accent}>_</Text>
</>
) : inputPlaceholder ? (
<Text color={colors.muted}>
{inputPlaceholder}
<Text color={colors.accent}>_</Text>
</Text>
) : (
<Text color={colors.accent}>_</Text>
)}
</Text>
</Box>
)
}
31 changes: 31 additions & 0 deletions packages/tui-components/src/execution/hooks/use-delegation-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export type DelegationTreeNode = {
parentRunId: string | undefined
childRunIds: string[]
totalTokens: number
inputTokens: number
outputTokens: number
cachedInputTokens: number
}

export type DelegationTreeState = {
Expand Down Expand Up @@ -130,6 +133,31 @@ export function getStatusCounts(state: DelegationTreeState): {
return { running, waiting }
}

/**
* Flatten the entire tree without pruning — includes all nodes regardless of status.
* Useful for testing and debugging where the full tree structure matters.
*/
export function flattenTreeAll(state: DelegationTreeState): FlatTreeNode[] {
if (!state.rootRunId) return []
const root = state.nodes.get(state.rootRunId)
if (!root) return []

const result: FlatTreeNode[] = []

function dfs(nodeId: string, depth: number, isLast: boolean, ancestorIsLast: boolean[]) {
const node = state.nodes.get(nodeId)
if (!node) return
result.push({ node, depth, isLast, ancestorIsLast: [...ancestorIsLast] })
for (let i = 0; i < node.childRunIds.length; i++) {
const childIsLast = i === node.childRunIds.length - 1
dfs(node.childRunIds[i]!, depth + 1, childIsLast, [...ancestorIsLast, isLast])
}
}

dfs(state.rootRunId, 0, true, [])
return result
}

export function flattenTree(state: DelegationTreeState): FlatTreeNode[] {
if (!state.rootRunId) return []
const root = state.nodes.get(state.rootRunId)
Expand Down Expand Up @@ -236,6 +264,9 @@ export function processDelegationTreeEvent(
parentRunId,
childRunIds: [],
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
cachedInputTokens: 0,
}

state.nodes.set(event.runId, node)
Expand Down
3 changes: 3 additions & 0 deletions packages/tui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

export type { ExecutionParams, ExecutionResult } from "./execution/index.js"
export { renderExecution } from "./execution/index.js"
export type { JobListItem, LogViewerParams, LogViewerScreen, RunInfo } from "./log-viewer/index.js"
// Log Viewer
export { renderLogViewer } from "./log-viewer/index.js"
export type { SelectionParams, SelectionResult } from "./selection/index.js"
// Selection
export { renderSelection } from "./selection/index.js"
Expand Down
Loading
Loading