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
12 changes: 12 additions & 0 deletions .changeset/add-event-detail-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@perstack/core": patch
"@perstack/runtime": patch
"@perstack/api-client": patch
"@perstack/base": patch
"@perstack/tui": patch
"perstack": patch
---

Add event detail view in history browser

Users can now select an event in the events list to view its details including type, step number, timestamp, and IDs.
35 changes: 14 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pnpm build # Build all packages
pnpm typecheck # Type check all packages
pnpm test # Run unit tests
pnpm format-and-lint # Run Biome linter/formatter
pnpm changeset # Create changeset for versioning (user runs this)
pnpm changeset # Create changeset for versioning
```

## Code Style
Expand Down Expand Up @@ -282,34 +282,27 @@ major.minor.patch

### Changeset Workflow

**IMPORTANT:** The user must run `pnpm changeset` themselves. Do NOT run it automatically.
When changes require a changeset, create a changeset file directly in `.changeset/` directory.

When changes require a changeset, provide the user with a draft including:
1. Which packages to select
2. Version bump type for each package
3. Changelog message content
**Changeset file format:**
```markdown
---
"@perstack/runtime": patch
---

**Example draft for user:**
```
Please run: pnpm changeset

Select packages:
- @perstack/runtime (patch)

Changelog message:
Fix memory leak in skill manager cleanup when MCP connection fails
```

**Bug fix:**
- Select: Only affected package
- Packages: Only affected package
- Type: patch

**New feature:**
- Select: @perstack/core + ALL packages (except docs)
- Packages: ALL packages (@perstack/core, @perstack/runtime, @perstack/api-client, @perstack/base, @perstack/tui, perstack)
- Type: minor (for all)

**Breaking change:**
- Select: ALL packages
- Packages: ALL packages
- Type: major (for all)

### Two-Stage Release Flow
Expand Down Expand Up @@ -385,14 +378,14 @@ When making changes, update ALL relevant documentation:
1. Edit code in appropriate package(s)
2. Run `pnpm typecheck && pnpm test`
3. Update related documentation
4. Provide changeset draft to user (ALL packages, minor)
4. Create changeset (ALL packages, minor)
5. Commit with `Add: <subject>` format

### Fixing a Bug

1. Edit code and add test
2. Run `pnpm typecheck && pnpm test`
3. Provide changeset draft to user (affected package only, patch)
3. Create changeset (affected package only, patch)
4. Commit with `Fix: <subject>` format

### Modifying Core Schemas
Expand All @@ -401,7 +394,7 @@ When making changes, update ALL relevant documentation:
2. Update all dependent packages if needed
3. Run `pnpm typecheck && pnpm test`
4. Update `docs/references/` and `packages/core/README.md`
5. Provide changeset draft to user (ALL packages, minor for optional, major for breaking)
5. Create changeset (ALL packages, minor for optional, major for breaking)

### Adding a New Tool to @perstack/base

Expand Down Expand Up @@ -595,7 +588,7 @@ pick = ["attemptCompletion", "think"]
- [ ] Review commit history (`git log --oneline -10`) and match style
- [ ] Commits are split into meaningful units (not monolithic)
- [ ] Related documentation updated (`docs/`, `README.md`, `packages/**/README.md`)
- [ ] Notify user to run `pnpm changeset` with draft provided
- [ ] Changeset created if needed

## Pre-push Checklist

Expand Down
15 changes: 14 additions & 1 deletion packages/tui/src/components/browser-router.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useCallback } from "react"
import { useInputAreaContext } from "../context/index.js"
import { assertNever } from "../helpers.js"
import type { InputState } from "../types/index.js"
import type { EventHistoryItem, InputState } from "../types/index.js"
import {
BrowsingCheckpointsInput,
BrowsingEventDetailInput,
BrowsingEventsInput,
BrowsingExpertsInput,
BrowsingHistoryInput,
Expand All @@ -14,6 +16,14 @@ type BrowserRouterProps = {
}
export const BrowserRouter = ({ inputState }: BrowserRouterProps) => {
const ctx = useInputAreaContext()
const handleEventSelect = useCallback(
(event: EventHistoryItem) => {
if (inputState.type === "browsingEvents") {
ctx.onEventSelect(inputState, event)
}
},
[ctx.onEventSelect, inputState],
)
switch (inputState.type) {
case "browsingHistory":
return (
Expand Down Expand Up @@ -47,9 +57,12 @@ export const BrowserRouter = ({ inputState }: BrowserRouterProps) => {
<BrowsingEventsInput
checkpoint={inputState.checkpoint}
events={inputState.events}
onEventSelect={handleEventSelect}
onBack={ctx.onBack}
/>
)
case "browsingEventDetail":
return <BrowsingEventDetailInput event={inputState.selectedEvent} onBack={ctx.onBack} />
default:
return assertNever(inputState)
}
Expand Down
46 changes: 46 additions & 0 deletions packages/tui/src/components/input-areas/browsing-event-detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Box, Text, useInput } from "ink"
import { KEY_HINTS } from "../../constants.js"
import { formatTimestamp } from "../../helpers.js"
import type { EventHistoryItem } from "../../types/index.js"

export type BrowsingEventDetailInputProps = {
event: EventHistoryItem
onBack: () => void
}
export const BrowsingEventDetailInput = ({ event, onBack }: BrowsingEventDetailInputProps) => {
useInput((input, key) => {
if (input === "b" || key.escape) {
onBack()
}
})
return (
<Box flexDirection="column" paddingX={1}>
<Box marginBottom={1}>
<Text bold>Event Detail</Text>
<Text dimColor> {KEY_HINTS.BACK}</Text>
</Box>
<Box flexDirection="column" marginLeft={2}>
<Text>
<Text color="gray">Type: </Text>
<Text color="cyan">{event.type}</Text>
</Text>
<Text>
<Text color="gray">Step: </Text>
<Text>{event.stepNumber}</Text>
</Text>
<Text>
<Text color="gray">Timestamp: </Text>
<Text>{formatTimestamp(event.timestamp)}</Text>
</Text>
<Text>
<Text color="gray">ID: </Text>
<Text dimColor>{event.id}</Text>
</Text>
<Text>
<Text color="gray">Run ID: </Text>
<Text dimColor>{event.runId}</Text>
</Text>
</Box>
</Box>
)
}
14 changes: 10 additions & 4 deletions packages/tui/src/components/input-areas/browsing-events.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { Text } from "ink"
import { KEY_HINTS } from "../../constants.js"
import { formatTimestamp, noop } from "../../helpers.js"
import { formatTimestamp } from "../../helpers.js"
import type { CheckpointHistoryItem, EventHistoryItem } from "../../types/index.js"
import { ListBrowser } from "../list-browser.js"

export type BrowsingEventsInputProps = {
checkpoint: CheckpointHistoryItem
events: EventHistoryItem[]
onEventSelect: (event: EventHistoryItem) => void
onBack: () => void
}
export const BrowsingEventsInput = ({ checkpoint, events, onBack }: BrowsingEventsInputProps) => (
export const BrowsingEventsInput = ({
checkpoint,
events,
onEventSelect,
onBack,
}: BrowsingEventsInputProps) => (
<ListBrowser
title={`Events for Step ${checkpoint.stepNumber} ${KEY_HINTS.NAVIGATE} ${KEY_HINTS.BACK}`}
title={`Events for Step ${checkpoint.stepNumber} ${KEY_HINTS.NAVIGATE} ${KEY_HINTS.SELECT} ${KEY_HINTS.BACK}`}
items={events}
onSelect={noop}
onSelect={onEventSelect}
onBack={onBack}
emptyMessage="No events found"
renderItem={(ev, isSelected) => (
Expand Down
4 changes: 4 additions & 0 deletions packages/tui/src/components/input-areas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export {
BrowsingCheckpointsInput,
type BrowsingCheckpointsInputProps,
} from "./browsing-checkpoints.js"
export {
BrowsingEventDetailInput,
type BrowsingEventDetailInputProps,
} from "./browsing-event-detail.js"
export { BrowsingEventsInput, type BrowsingEventsInputProps } from "./browsing-events.js"
export { BrowsingExpertsInput, type BrowsingExpertsInputProps } from "./browsing-experts.js"
export { BrowsingHistoryInput, type BrowsingHistoryInputProps } from "./browsing-history.js"
8 changes: 7 additions & 1 deletion packages/tui/src/context/input-area-context.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { createContext, useContext } from "react"
import type { CheckpointHistoryItem, RunHistoryItem } from "../types/index.js"
import type {
BrowsingEventsState,
CheckpointHistoryItem,
EventHistoryItem,
RunHistoryItem,
} from "../types/index.js"
export type InputAreaContextValue = {
onExpertSelect: (expertKey: string) => void
onQuerySubmit: (query: string) => void
onRunSelect: (run: RunHistoryItem) => void
onRunResume: (run: RunHistoryItem) => void
onCheckpointSelect: (checkpoint: CheckpointHistoryItem) => void
onCheckpointResume: (checkpoint: CheckpointHistoryItem) => void
onEventSelect: (state: BrowsingEventsState, event: EventHistoryItem) => void
onBack: () => void
onSwitchToExperts: () => void
onSwitchToHistory: () => void
Expand Down
1 change: 0 additions & 1 deletion packages/tui/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const truncateText = (text: string, maxLength: number): string => {
export const assertNever = (x: never): never => {
throw new Error(`Unexpected value: ${x}`)
}
export const noop = (): void => {}
export const formatTimestamp = (timestamp: number): string => new Date(timestamp).toLocaleString()
export const shortenPath = (fullPath: string, maxLength = 60): string => {
if (fullPath.length <= maxLength) return fullPath
Expand Down
33 changes: 30 additions & 3 deletions packages/tui/src/hooks/actions/use-history-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useCallback, useState } from "react"
import type {
BrowsingEventDetailState,
BrowsingEventsState,
CheckpointHistoryItem,
EventHistoryItem,
ExpertOption,
Expand Down Expand Up @@ -146,9 +148,33 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => {
dispatch({ type: "GO_BACK_FROM_CHECKPOINTS", historyRuns })
}
}, [historyRuns, dispatch])
const handleEventSelect = useCallback(
(state: BrowsingEventsState, event: EventHistoryItem) => {
dispatch({
type: "SELECT_EVENT",
checkpoint: state.checkpoint,
events: state.events,
selectedEvent: event,
})
},
[dispatch],
)
const handleBackFromEventDetail = useCallback(
(state: BrowsingEventDetailState) => {
dispatch({
type: "GO_BACK_FROM_EVENT_DETAIL",
checkpoint: state.checkpoint,
events: state.events,
})
},
[dispatch],
)
const handleBack = useCallback(
(currentStateType: InputState["type"]) => {
switch (currentStateType) {
(currentState: InputState) => {
switch (currentState.type) {
case "browsingEventDetail":
handleBackFromEventDetail(currentState)
break
case "browsingEvents":
handleBackFromEvents()
break
Expand All @@ -157,7 +183,7 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => {
break
}
},
[handleBackFromEvents, handleBackFromCheckpoints],
[handleBackFromEventDetail, handleBackFromEvents, handleBackFromCheckpoints],
)
const handleSwitchToExperts = useCallback(() => {
dispatch({ type: "BROWSE_EXPERTS", experts: allExperts })
Expand All @@ -173,6 +199,7 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => {
handleRunResume,
handleCheckpointSelect,
handleCheckpointResume,
handleEventSelect,
handleBack,
handleSwitchToExperts,
handleSwitchToHistory,
Expand Down
24 changes: 24 additions & 0 deletions packages/tui/src/hooks/state/use-input-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ type InputAction =
| { type: "GO_BACK_FROM_EVENTS"; run: RunHistoryItem; checkpoints: CheckpointHistoryItem[] }
| { type: "GO_BACK_FROM_CHECKPOINTS"; historyRuns: RunHistoryItem[] }
| { type: "INITIALIZE_RUNTIME" }
| {
type: "SELECT_EVENT"
checkpoint: CheckpointHistoryItem
events: EventHistoryItem[]
selectedEvent: EventHistoryItem
}
| {
type: "GO_BACK_FROM_EVENT_DETAIL"
checkpoint: CheckpointHistoryItem
events: EventHistoryItem[]
}
const inputReducer = (_state: InputState, action: InputAction): InputState => {
switch (action.type) {
case "SELECT_EXPERT":
Expand Down Expand Up @@ -55,6 +66,19 @@ const inputReducer = (_state: InputState, action: InputAction): InputState => {
return { type: "browsingCheckpoints", run: action.run, checkpoints: action.checkpoints }
case "GO_BACK_FROM_CHECKPOINTS":
return { type: "browsingHistory", runs: action.historyRuns }
case "SELECT_EVENT":
return {
type: "browsingEventDetail",
checkpoint: action.checkpoint,
events: action.events,
selectedEvent: action.selectedEvent,
}
case "GO_BACK_FROM_EVENT_DETAIL":
return {
type: "browsingEvents",
checkpoint: action.checkpoint,
events: action.events,
}
default:
return assertNever(action)
}
Expand Down
6 changes: 4 additions & 2 deletions packages/tui/src/hooks/use-app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export const useAppState = (props: UseAppStateProps) => {
setExpertName,
})
const handleBack = useCallback(() => {
history.handleBack(inputState.type)
}, [history.handleBack, inputState.type])
history.handleBack(inputState)
}, [history.handleBack, inputState])
const inputAreaContextValue = useMemo<InputAreaContextValue>(
() => ({
onExpertSelect: handleExpertSelect,
Expand All @@ -120,6 +120,7 @@ export const useAppState = (props: UseAppStateProps) => {
onRunResume: history.handleRunResume,
onCheckpointSelect: history.handleCheckpointSelect,
onCheckpointResume: history.handleCheckpointResume,
onEventSelect: history.handleEventSelect,
onBack: handleBack,
onSwitchToExperts: history.handleSwitchToExperts,
onSwitchToHistory: history.handleSwitchToHistory,
Expand All @@ -131,6 +132,7 @@ export const useAppState = (props: UseAppStateProps) => {
history.handleRunResume,
history.handleCheckpointSelect,
history.handleCheckpointResume,
history.handleEventSelect,
handleBack,
history.handleSwitchToExperts,
history.handleSwitchToHistory,
Expand Down
1 change: 1 addition & 0 deletions packages/tui/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
} from "./base.js"
export type {
BrowsingCheckpointsState,
BrowsingEventDetailState,
BrowsingEventsState,
BrowsingExpertsState,
BrowsingHistoryState,
Expand Down
Loading