Skip to content
Open
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
1 change: 1 addition & 0 deletions plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ When a plan ships, leave the file in place and update the **Status** line so we
- [`sqlite-cache.md`](./sqlite-cache.md) — persistent SQLite cache for queues, hydrated details, comments, and optional diffs.
- [`cache-v2.md`](./cache-v2.md) — audit-driven follow-up: diff cache, per-repo metadata persistence, `--cache-info` / `--cache-clear`.
- [`comments-pane-redesign.md`](./comments-pane-redesign.md) — living design doc exploring how the Comments pane should render. Multiple styles, fully specced, iterate freely.
- [`actions-view.md`](./actions-view.md) — full-screen Actions view for workflow runs, job graph, logs, and live refresh.
45 changes: 45 additions & 0 deletions plans/actions-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Actions View

- **Why**
- ghui already shows check rollups, but there is no way to inspect workflow runs, job status, or logs without leaving the TUI.
- PR review flow needs quick insight into failing checks and in-progress pipelines.

- **What we'd ship**
- A full-screen Actions view opened from PR detail with `a`.
- Workflow run list for the PR head SHA.
- Drill-down to workflow jobs, with an ASCII dependency overview derived from workflow YAML `needs`.
- Drill-down to job logs (scrollable, job-level logs).
- Live refresh while any run is still queued/in-progress.

- **API / architecture mapping**
- `src/services/GitHubService.ts`
- Extend check GraphQL fragment with `databaseId`, `detailsUrl`, workflow run metadata.
- Add Actions REST calls:
- `listWorkflowRunsForPullRequest(repository, headSha)`
- `getWorkflowRunJobs(repository, runId)`
- `getWorkflowJobLog(repository, jobId)`
- `getWorkflowRunDependencies(repository, runId, headSha)`
- `src/domain.ts`
- Add `WorkflowRun`, `WorkflowJob`, `WorkflowStep`, `WorkflowJobDependency`.
- Extend `CheckItem` with optional workflow linkage metadata.
- `src/App.tsx`
- Add actions-view atoms/state and navigation stack (`runs` -> `jobs` -> `logs`).
- Add live-refresh polling interval for in-flight runs.
- `src/ui/ActionsPane.tsx`
- New full-screen actions UI.
- `src/ui/workflowGraph.ts`
- Render ASCII dependency rows from parsed workflow dependencies.
- `src/keymap/actionsView.ts`
- Key bindings for navigation, open, refresh, back.

- **Open questions**
- Should we support per-step log filtering (failed-only) in v2?
- Should we open selected job URL directly (if available) instead of run/PR URL fallback?

- **Out of scope (for v1)**
- Re-run/cancel workflow actions.
- Step-level log folding UI.
- True graph-layout engine beyond the compact ASCII dependency rows.

- **Status**
- In progress.
694 changes: 657 additions & 37 deletions src/App.tsx

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions src/appCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ interface AppCommandActions {
readonly closeDiffView: () => void
readonly openCommentsView: () => void
readonly closeCommentsView: () => void
readonly openActionsView: () => void
readonly closeActionsView: () => void
readonly refreshActionsView: () => void
readonly openSelectedActionInBrowser: () => void
readonly openNewIssueCommentModal: () => void
readonly openReplyToSelectedComment: () => void
readonly openEditSelectedComment: () => void
Expand Down Expand Up @@ -57,6 +61,7 @@ interface BuildAppCommandsInput {
readonly detailFullView: boolean
readonly diffFullView: boolean
readonly commentsViewActive: boolean
readonly actionsViewActive: boolean
readonly hasSelectedComment: boolean
readonly canEditSelectedComment: boolean
readonly diffReady: boolean
Expand Down Expand Up @@ -86,6 +91,7 @@ export const buildAppCommands = ({
detailFullView,
diffFullView,
commentsViewActive,
actionsViewActive,
hasSelectedComment,
canEditSelectedComment,
diffReady,
Expand Down Expand Up @@ -227,6 +233,41 @@ export const buildAppCommands = ({
keywords: ["conversation", "discussion", "review"],
run: actions.openCommentsView,
}),
forSelected({
id: "actions.open",
title: "Open actions",
scope: "Actions",
shortcut: "a",
keywords: ["checks", "workflows", "jobs", "logs"],
run: actions.openActionsView,
}),
defineCommand({
id: "actions.close",
title: "Close actions view",
scope: "Actions",
subtitle: "Return to pull request detail",
shortcut: "esc",
disabledReason: actionsViewActive ? null : "Actions view is not open.",
run: actions.closeActionsView,
}),
defineCommand({
id: "actions.refresh",
title: "Refresh actions",
scope: "Actions",
subtitle: selectedPullRequestLabel,
shortcut: "r",
disabledReason: actionsViewActive && selectedPullRequest ? null : "Open actions first.",
run: actions.refreshActionsView,
}),
defineCommand({
id: "actions.open-browser",
title: "Open selected action in browser",
scope: "Actions",
subtitle: selectedPullRequestLabel,
shortcut: "o",
disabledReason: actionsViewActive && selectedPullRequest ? null : "Open actions first.",
run: actions.openSelectedActionInBrowser,
}),
forSelected({
id: "comments.new",
title: "New comment",
Expand Down
4 changes: 2 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type CommandScope = "Global" | "View" | "Pull request" | "Diff" | "Comments" | "Navigation" | "System"
export type CommandScope = "Global" | "View" | "Pull request" | "Diff" | "Comments" | "Actions" | "Navigation" | "System"

const SCOPE_ORDER: readonly CommandScope[] = ["Global", "View", "Pull request", "Diff", "Comments", "Navigation", "System"]
const SCOPE_ORDER: readonly CommandScope[] = ["Global", "View", "Pull request", "Diff", "Comments", "Actions", "Navigation", "System"]

export const sortCommandsByScope = (commands: readonly AppCommand[]) => [...commands].sort((left, right) => SCOPE_ORDER.indexOf(left.scope) - SCOPE_ORDER.indexOf(right.scope))

Expand Down
40 changes: 40 additions & 0 deletions src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,46 @@ export interface CheckItem {
readonly name: string
readonly status: CheckRunStatus
readonly conclusion: CheckConclusion | null
readonly databaseId?: number
readonly detailsUrl?: string | null
readonly workflowRunId?: number
readonly workflowName?: string | null
}

export interface WorkflowStep {
readonly number: number
readonly name: string
readonly status: CheckRunStatus
readonly conclusion: CheckConclusion | null
}

export interface WorkflowJob {
readonly id: number
readonly name: string
readonly status: CheckRunStatus
readonly conclusion: CheckConclusion | null
readonly startedAt: Date | null
readonly completedAt: Date | null
readonly steps: readonly WorkflowStep[]
}

export interface WorkflowRun {
readonly id: number
readonly name: string
readonly status: CheckRunStatus
readonly conclusion: CheckConclusion | null
readonly url: string
readonly event: string
readonly branch: string
readonly createdAt: Date | null
readonly updatedAt: Date | null
readonly jobs: readonly WorkflowJob[]
}

export interface WorkflowJobDependency {
readonly id: string
readonly name: string
readonly needs: readonly string[]
}

export interface PullRequestLabel {
Expand Down
56 changes: 56 additions & 0 deletions src/keymap/actionsView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { context, type Scrollable, scrollCommands } from "@ghui/keymap"

export interface ActionsViewCtx extends Scrollable {
readonly closeOrBack: () => void
readonly confirmSelection: () => void
readonly refresh: () => void
readonly openInBrowser: () => void
readonly stepBy: (delta: number) => void
readonly toggleWrap: () => void
readonly scrollHorizontal: (delta: number, halfPage?: boolean) => void
readonly enterFilter: () => void
readonly applyFilter: () => void
readonly cancelFilter: () => void
readonly filterActive: boolean
readonly canFilterLogs: boolean
readonly toggleGraphModal: () => void
readonly canShowGraph: boolean
readonly jumpToNextMatch: () => void
readonly jumpToPreviousMatch: () => void
readonly hasFilterQuery: boolean
}

const Actions = context<ActionsViewCtx>()

export const actionsViewKeymap = Actions(
scrollCommands<ActionsViewCtx>(),
{ id: "actions.previous-step", title: "Collapse step", keys: ["left", "h"], run: (s) => s.stepBy(-1) },
{ id: "actions.next-step", title: "Expand step", keys: ["right", "l"], run: (s) => s.stepBy(1) },
{ id: "actions.toggle-wrap", title: "Toggle log wrap", keys: ["w"], when: (s) => s.canFilterLogs && !s.filterActive, run: (s) => s.toggleWrap() },
{ id: "actions.scroll-horizontal-left", title: "Scroll log left", keys: ["z h"], when: (s) => s.canFilterLogs && !s.filterActive, run: (s) => s.scrollHorizontal(-4) },
{ id: "actions.scroll-horizontal-right", title: "Scroll log right", keys: ["z l"], when: (s) => s.canFilterLogs && !s.filterActive, run: (s) => s.scrollHorizontal(4) },
{
id: "actions.scroll-horizontal-half-left",
title: "Scroll log half left",
keys: ["z shift+h"],
when: (s) => s.canFilterLogs && !s.filterActive,
run: (s) => s.scrollHorizontal(-1, true),
},
{
id: "actions.scroll-horizontal-half-right",
title: "Scroll log half right",
keys: ["z shift+l"],
when: (s) => s.canFilterLogs && !s.filterActive,
run: (s) => s.scrollHorizontal(1, true),
},
{ id: "actions.filter", title: "Filter logs", keys: ["/"], when: (s) => s.canFilterLogs && !s.filterActive, run: (s) => s.enterFilter() },
{ id: "actions.cancel-filter", title: "Cancel log filter", keys: ["escape"], when: (s) => s.filterActive, run: (s) => s.cancelFilter() },
{ id: "actions.apply-filter", title: "Apply log filter", keys: ["return"], when: (s) => s.filterActive, run: (s) => s.applyFilter() },
{ id: "actions.next-match", title: "Next match", keys: ["n"], when: (s) => s.hasFilterQuery && !s.filterActive, run: (s) => s.jumpToNextMatch() },
{ id: "actions.previous-match", title: "Previous match", keys: ["shift+n"], when: (s) => s.hasFilterQuery && !s.filterActive, run: (s) => s.jumpToPreviousMatch() },
{ id: "actions.close", title: "Back / close actions", keys: ["escape", "a"], when: (s) => !s.filterActive, run: (s) => s.closeOrBack() },
{ id: "actions.confirm", title: "Open selected action", keys: ["return"], when: (s) => !s.filterActive, run: (s) => s.confirmSelection() },
{ id: "actions.refresh", title: "Refresh actions", keys: ["r"], when: (s) => !s.filterActive, run: (s) => s.refresh() },
{ id: "actions.open-browser", title: "Open in browser", keys: ["o"], when: (s) => !s.filterActive, run: (s) => s.openInBrowser() },
{ id: "actions.graph-modal", title: "Show graph", keys: ["s"], when: (s) => s.canShowGraph && !s.filterActive, run: (s) => s.toggleGraphModal() },
)
6 changes: 5 additions & 1 deletion src/keymap/all.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { context } from "@ghui/keymap"
import { changedFilesModalKeymap, type ChangedFilesModalCtx } from "./changedFilesModal.ts"
import { actionsViewKeymap, type ActionsViewCtx } from "./actionsView.ts"
import { closeModalKeymap, type CloseModalCtx } from "./closeModal.ts"
import { commandPaletteKeymap, type CommandPaletteCtx } from "./commandPalette.ts"
import { commentModalKeymap, type CommentModalCtx } from "./commentModal.ts"
Expand Down Expand Up @@ -35,6 +36,7 @@ export interface AppCtx {
readonly diffFullView: boolean
readonly detailFullView: boolean
readonly commentsViewActive: boolean
readonly actionsViewActive: boolean

// True whenever a modal/mode swallows raw text input (so q-quit, etc. are
// disabled inside text-editing contexts).
Expand All @@ -57,6 +59,7 @@ export interface AppCtx {
readonly diff: DiffViewCtx
readonly detail: DetailViewCtx
readonly commentsView: CommentsViewCtx
readonly actionsView: ActionsViewCtx
readonly listNav: ListNavCtx

// Always-on / app-level
Expand All @@ -80,7 +83,7 @@ const modalActive = (a: AppCtx): boolean =>
a.deleteCommentModalActive ||
a.commandPaletteActive

const inListMode = (a: AppCtx): boolean => !modalActive(a) && !a.filterMode && !a.diffFullView && !a.detailFullView && !a.commentsViewActive
const inListMode = (a: AppCtx): boolean => !modalActive(a) && !a.filterMode && !a.diffFullView && !a.detailFullView && !a.commentsViewActive && !a.actionsViewActive

export const appKeymap = App(
// Always-on: command palette opener
Expand Down Expand Up @@ -121,6 +124,7 @@ export const appKeymap = App(
diffViewKeymap.scope((a) => a.diffFullView && !modalActive(a) && a.diff),
detailViewKeymap.scope((a) => a.detailFullView && !modalActive(a) && a.detail),
commentsViewKeymap.scope((a) => a.commentsViewActive && !modalActive(a) && a.commentsView),
actionsViewKeymap.scope((a) => a.actionsViewActive && !modalActive(a) && a.actionsView),

// PR list nav
listNavKeymap.scope((a) => inListMode(a) && a.listNav),
Expand Down
2 changes: 2 additions & 0 deletions src/keymap/detailView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface DetailViewCtx extends Scrollable {
readonly openDiff: () => void
readonly openComments: () => void
readonly openReview: () => void
readonly openActions: () => void
readonly closePullRequest: () => void
readonly openLabels: () => void
readonly openMerge: () => void
Expand All @@ -22,6 +23,7 @@ export const detailViewKeymap = Detail(
{ id: "detail.close", title: "Close detail", keys: ["escape", "return"], run: (s) => s.closeDetail() },
{ id: "detail.theme", title: "Open theme", keys: ["t"], run: (s) => s.openTheme() },
{ id: "detail.diff", title: "Open diff", keys: ["d"], run: (s) => s.openDiff() },
{ id: "detail.actions", title: "Open actions", keys: ["a"], run: (s) => s.openActions() },
{ id: "detail.comments", title: "Open comments", keys: ["c"], run: (s) => s.openComments() },
{ id: "detail.review", title: "Review pull request", keys: ["shift+r"], run: (s) => s.openReview() },
{ id: "detail.close-pr", title: "Close pull request", keys: ["x"], run: (s) => s.closePullRequest() },
Expand Down
Loading