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
61 changes: 60 additions & 1 deletion skills/linear-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Commands:
comment - Manage issue comments
attach <issueId> <filepath> - Attach a file to an issue
link <urlOrIssueId> [url] - Link a URL to an issue
relation - Manage issue relations (dependencies)
relation - Manage issue relations (dependencies)
agent-session - Manage agent sessions for an issue
```

## Subcommands
Expand Down Expand Up @@ -524,3 +525,61 @@ Options:
-h, --help - Show this help.
-w, --workspace <slug> - Target workspace (uses credentials)
```

### agent-session

> Manage agent sessions for an issue

```
Usage: linear issue agent-session

Description:

Manage agent sessions for an issue

Options:

-h, --help - Show this help.
-w, --workspace <slug> - Target workspace (uses credentials)

Commands:

list [issueId] - List agent sessions for an issue
view, v <sessionId> - View agent session details
```

#### agent-session subcommands

##### list

```
Usage: linear issue agent-session list [issueId]

Description:

List agent sessions for an issue

Options:

-h, --help - Show this help.
-w, --workspace <slug> - Target workspace (uses credentials)
-j, --json - Output as JSON
--status <status> - Filter by session status (Values: "pending", "active", "complete", "awaitingInput",
"error", "stale")
```

##### view

```
Usage: linear issue agent-session view <sessionId>

Description:

View agent session details

Options:

-h, --help - Show this help.
-w, --workspace <slug> - Target workspace (uses credentials)
-j, --json - Output as JSON
```
162 changes: 162 additions & 0 deletions src/commands/issue/issue-agent-session-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Command, EnumType } from "@cliffy/command"
import { unicodeWidth } from "@std/cli"
import { green, yellow } from "@std/fmt/colors"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import { padDisplay, truncateText } from "../../utils/display.ts"
import { getIssueIdentifier } from "../../utils/linear.ts"
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
import { header, muted } from "../../utils/styling.ts"
import { handleError, ValidationError } from "../../utils/errors.ts"

const GetIssueAgentSessions = gql(`
query GetIssueAgentSessions($issueId: String!) {
issue(id: $issueId) {
comments(first: 100) {
nodes {
agentSession {
id
status
type
createdAt
startedAt
endedAt
summary
creator {
name
}
appUser {
name
}
}
}
}
}
}
`)

function formatStatus(status: string): string {
switch (status) {
case "active":
return green(padDisplay("active", 13))
case "pending":
return yellow(padDisplay("pending", 13))
case "awaitingInput":
return yellow(padDisplay("awaitingInput", 13))
case "complete":
return muted(padDisplay("complete", 13))
case "error":
return padDisplay("error", 13)
case "stale":
return muted(padDisplay("stale", 13))
default:
return padDisplay(status, 13)
}
}

function formatDate(dateString: string): string {
return dateString.slice(0, 10)
}

const AgentSessionStatusType = new EnumType([
"pending",
"active",
"complete",
"awaitingInput",
"error",
"stale",
])

export const agentSessionListCommand = new Command()
.name("list")
.description("List agent sessions for an issue")
.type("agentSessionStatus", AgentSessionStatusType)
.arguments("[issueId:string]")
.option("-j, --json", "Output as JSON")
.option(
"--status <status:agentSessionStatus>",
"Filter by session status",
)
.action(async ({ json, status }, issueId) => {
try {
const resolvedIdentifier = await getIssueIdentifier(issueId)
if (!resolvedIdentifier) {
throw new ValidationError(
"Could not determine issue ID",
{ suggestion: "Please provide an issue ID like 'ENG-123'." },
)
}

const { Spinner } = await import("@std/cli/unstable-spinner")
const showSpinner = shouldShowSpinner()
const spinner = showSpinner ? new Spinner() : null
spinner?.start()

const client = getGraphQLClient()
const result = await client.request(GetIssueAgentSessions, {
issueId: resolvedIdentifier,
})
spinner?.stop()

let sessions = (result.issue?.comments?.nodes || [])
.map((c) => c.agentSession)
.filter((s): s is NonNullable<typeof s> => s != null)

if (status) {
sessions = sessions.filter((s) => s.status === status)
}

if (json) {
console.log(JSON.stringify(sessions, null, 2))
return
}

if (sessions.length === 0) {
console.log("No agent sessions found for this issue.")
return
}

const { columns } = Deno.stdout.isTerminal()
? Deno.consoleSize()
: { columns: 120 }

const STATUS_WIDTH = 13
const DATE_WIDTH = 10
const AGENT_WIDTH = Math.max(
5,
...sessions.map((s) => unicodeWidth(s.appUser.name)),
)
const SPACE_WIDTH = 3

const fixed = STATUS_WIDTH + DATE_WIDTH + AGENT_WIDTH + SPACE_WIDTH
const PADDING = 1
const availableWidth = Math.max(columns - PADDING - fixed, 10)

const headerCells = [
padDisplay("STATUS", STATUS_WIDTH),
padDisplay("AGENT", AGENT_WIDTH),
padDisplay("CREATED", DATE_WIDTH),
"SUMMARY",
]

console.log(header(headerCells.join(" ")))

for (const session of sessions) {
const summaryText = session.summary
? truncateText(
session.summary.replace(/\n/g, " "),
availableWidth,
)
: muted("--")

const line = `${formatStatus(session.status)} ${
padDisplay(session.appUser.name, AGENT_WIDTH)
} ${
padDisplay(formatDate(session.createdAt), DATE_WIDTH)
} ${summaryText}`
console.log(line)
}
} catch (error) {
handleError(error, "Failed to list agent sessions")
}
})
Loading
Loading