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
24 changes: 24 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ Notes:
- shows orchestrator PID, dashboard URL, and configured projects when healthy
- prints orchestrator diagnostics when the runtime has issues

## parallax tasks

List the 20 most recent tasks with their current status, AI adapter, and model.

```bash
parallax tasks
```

Outputs a table with the following columns:

| Column | Description |
|--------|-------------|
| TASK ID | External issue ID (e.g. `PROJ-42`) or internal task ID if no external ID is set |
| NAME | Task title, truncated to 50 characters |
| ADAPTER | Agent provider (`claude-code`, `codex`, `gemini`) |
| MODEL | Model configured for the project (`—` if unset) |
| STATUS | Color-coded: green=done, cyan=running, yellow=queued, red=failed, dim=canceled |

Notes:

- no flags accepted
- requires Parallax to be running (`parallax start`)
- shows tasks sorted by most recently created, newest first

## parallax start

Start orchestrator and dashboard in background.
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
StartCommandOptions,
StopCommandOptions,
StatusCommandOptions,
TasksCommandOptions,
} from './types.js'

export function parseArg(args: string[], key: string): string | undefined {
Expand Down Expand Up @@ -197,6 +198,14 @@ export function parseStatusOptions(args: string[]): StatusCommandOptions {
return {}
}

export function parseTasksOptions(args: string[]): TasksCommandOptions {
if (args.length > 0) {
throw new Error('parallax tasks does not accept flags.')
}

return {}
}

export function resolvePath(raw: string): string {
return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw)
}
128 changes: 128 additions & 0 deletions packages/cli/src/commands/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { CliContext } from '../types.js'

type TaskEntry = {
id: string
externalId: string
title: string
status: string
projectId: string
createdAt: number
}

type ProjectEntry = {
id: string
agent: { provider: string; model?: string }
}

const GREEN = '\x1b[32m'
const RED = '\x1b[31m'
const YELLOW = '\x1b[33m'
const CYAN = '\x1b[36m'
const DIM = '\x1b[2m'
const RESET = '\x1b[0m'
const BOLD = '\x1b[1m'

function colorStatus(status: string): string {
switch (status) {
case 'done':
return `${GREEN}${status}${RESET}`
case 'running':
return `${CYAN}${status}${RESET}`
case 'queued':
return `${YELLOW}${status}${RESET}`
case 'failed':
return `${RED}${status}${RESET}`
case 'canceled':
return `${DIM}${status}${RESET}`
default:
return status
}
}

export async function runTasks(_args: string[], context: CliContext) {
let apiBase: string
try {
apiBase = await context.resolveDefaultApiBase()
} catch {
throw new Error("Parallax is not running. Start it first with 'parallax start'.")
}

const [tasksRes, configRes] = await Promise.all([
fetch(`${apiBase}/tasks`),
fetch(`${apiBase}/config`),
])

if (!tasksRes.ok) {
throw new Error(`Failed to fetch tasks (${tasksRes.status}): ${tasksRes.statusText}`)
}
if (!configRes.ok) {
throw new Error(`Failed to fetch config (${configRes.status}): ${configRes.statusText}`)
}

const allTasks = (await tasksRes.json()) as TaskEntry[]
const config = (await configRes.json()) as { projects?: ProjectEntry[] }
const projects = new Map((config.projects ?? []).map((p) => [p.id, p]))

const tasks = allTasks
.slice()
.sort((a, b) => b.createdAt - a.createdAt)
.slice(0, 20)

if (tasks.length === 0) {
console.log('No tasks found.')
return
}

const rows = tasks.map((task) => {
const project = projects.get(task.projectId)
const provider = project?.agent.provider ?? '—'
const model = project?.agent.model ?? '—'
const displayId = task.externalId || task.id
const title = task.title.length > 50 ? task.title.slice(0, 47) + '...' : task.title
return { id: displayId, title, provider, model, status: task.status }
})

const colWidths = {
id: Math.max(7, ...rows.map((r) => r.id.length)),
title: Math.max(5, ...rows.map((r) => r.title.length)),
provider: Math.max(8, ...rows.map((r) => r.provider.length)),
model: Math.max(5, ...rows.map((r) => r.model.length)),
status: Math.max(6, ...rows.map((r) => r.status.length)),
}

const pad = (s: string, n: number) => s.padEnd(n)

const header = [
pad('TASK ID', colWidths.id),
pad('NAME', colWidths.title),
pad('ADAPTER', colWidths.provider),
pad('MODEL', colWidths.model),
pad('STATUS', colWidths.status),
].join(' ')

const divider = [
'─'.repeat(colWidths.id),
'─'.repeat(colWidths.title),
'─'.repeat(colWidths.provider),
'─'.repeat(colWidths.model),
'─'.repeat(colWidths.status),
].join(' ')

console.log()
console.log(`${BOLD}${header}${RESET}`)
console.log(`${DIM}${divider}${RESET}`)

for (const row of rows) {
console.log(
[
pad(row.id, colWidths.id),
pad(row.title, colWidths.title),
pad(row.provider, colWidths.provider),
pad(row.model, colWidths.model),
colorStatus(row.status),
].join(' ')
)
}

console.log()
}
6 changes: 6 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseRetryOptions,
parseStartOptions,
parseStatusOptions,
parseTasksOptions,
parseStopOptions as parseStopOptionsInternal,
resolvePath,
} from './args.js'
Expand All @@ -34,6 +35,7 @@ import { runRetry } from './commands/retry.js'
import { runStart } from './commands/start.js'
import { runStatus } from './commands/status.js'
import { runStop } from './commands/stop.js'
import { runTasks } from './commands/tasks.js'
import type { CliContext } from './types.js'
import { printUsage } from './usage.js'

Expand Down Expand Up @@ -164,6 +166,9 @@ async function cli() {
case 'logs':
await runLogs(commandArgs, cliContext)
return
case 'tasks':
await runTasks(commandArgs, cliContext)
return
default:
console.error(`Unknown command: ${command}\n`)
printUsage()
Expand All @@ -183,6 +188,7 @@ export {
parseRetryOptions,
parseStartOptions,
parseStatusOptions,
parseTasksOptions,
parseRunningState,
resolveDefaultApiBase,
resolvePath,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type PreflightCommandOptions = Record<string, never>

export type StatusCommandOptions = Record<string, never>

export type TasksCommandOptions = Record<string, never>

export type StartCommandOptions = {
apiPort: number
uiPort: number
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function printUsage(): void {
parallax start [--server-api-port <port>] [--server-ui-port <port>] [--concurrency <count>]
parallax stop
parallax status
parallax tasks
parallax open
parallax preflight
parallax pr-review <task-id>
Expand All @@ -18,6 +19,7 @@ Commands:
start Start orchestrator + UI in background.
stop Force-stop the running Parallax processes.
status Show orchestrator state and configured projects.
tasks List the last 20 tasks with their status, AI adapter, and model.
open Open the dashboard in your browser.
preflight Validate local prerequisites and auth.
pr-review [experimental] Apply open human PR review comments to the task's existing open PR.
Expand Down
Loading
Loading