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
4 changes: 4 additions & 0 deletions src/main/__tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ describe('listIssues', () => {
{
number: 10,
title: 'Bug report',
body: 'Something is broken',
state: 'OPEN',
author: { login: 'alice' },
labels: [{ name: 'bug' }, { name: 'critical' }],
Expand All @@ -193,6 +194,7 @@ describe('listIssues', () => {
{
number: 11,
title: 'Done issue',
body: '',
state: 'CLOSED',
author: { login: 'charlie' },
labels: [],
Expand All @@ -208,9 +210,11 @@ describe('listIssues', () => {

expect(issues).toHaveLength(2)
expect(issues[0].state).toBe('open')
expect(issues[0].body).toBe('Something is broken')
expect(issues[0].labels).toEqual(['bug', 'critical'])
expect(issues[0].assignees).toEqual(['bob'])
expect(issues[1].state).toBe('closed')
expect(issues[1].body).toBe('')
expect(issues[1].assignees).toEqual([])
})

Expand Down
4 changes: 3 additions & 1 deletion src/main/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function listIssues(
if (!ghRepo) return []

const nwo = `${ghRepo.owner}/${ghRepo.repo}`
const fields = 'number,title,state,author,labels,assignees,url,createdAt,updatedAt'
const fields = 'number,title,body,state,author,labels,assignees,url,createdAt,updatedAt'

return new Promise((resolve, reject) => {
execFile(
Expand All @@ -110,6 +110,7 @@ export async function listIssues(
(issue: {
number: number
title: string
body: string
state: string
author: { login: string }
labels: { name: string }[]
Expand All @@ -120,6 +121,7 @@ export async function listIssues(
}) => ({
number: issue.number,
title: issue.title,
body: issue.body ?? '',
state: issue.state === 'CLOSED' ? 'closed' : 'open',
author: issue.author?.login ?? '',
labels: (issue.labels ?? []).map((l) => l.name),
Expand Down
11 changes: 9 additions & 2 deletions src/main/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function spawnClaude(
name: string,
resume: boolean,
prompt?: string,
systemPrompt?: string,
env?: Record<string, string>
): nodePty.IPty {
const args = resume
Expand All @@ -146,8 +147,13 @@ function spawnClaude(
args.push('--plugin-dir', DEV_PLUGIN_PATH)
}

if (systemPrompt && !resume) {
args.push('--append-system-prompt', systemPrompt)
}

// prompt is a positional argument — must come last, after all flags
if (prompt && !resume) {
args.push('--prompt', prompt)
args.push(prompt)
}

log.info('session', `spawn: ${getClaudePath()} ${args.join(' ')} cwd=${cwd}`)
Expand All @@ -169,6 +175,7 @@ export function createSession(
name?: string
resume?: boolean
prompt?: string
systemPrompt?: string
envScript?: string
}
): { id: string; claudeSessionId: string } {
Expand All @@ -184,7 +191,7 @@ export function createSession(
`createSession id=${id} claude=${claudeSessionId} resume=${resume} cwd=${cwd}`
)

const pty = spawnClaude(cwd, claudeSessionId, name, resume, opts?.prompt, env)
const pty = spawnClaude(cwd, claudeSessionId, name, resume, opts?.prompt, opts?.systemPrompt, env)

const entry: SessionEntry = {
pty,
Expand Down
3 changes: 2 additions & 1 deletion src/main/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFile, writeFile, rename, mkdir } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
import type { PrInfo } from '../shared/types'
import type { PrInfo, IssueInfo } from '../shared/types'

const STORE_DIR = join(homedir(), '.konductor')
const STATE_FILE = join(STORE_DIR, 'state.json')
Expand All @@ -20,6 +20,7 @@ export interface SessionData {
summary: string
claudeSessionId: string
pr?: PrInfo
issue?: IssueInfo
}

export interface PersistedState {
Expand Down
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface KonductorAPI {
name?: string
resume?: boolean
prompt?: string
systemPrompt?: string
envScript?: string
}
) => Promise<{ id: string; claudeSessionId: string }>
Expand Down Expand Up @@ -103,6 +104,7 @@ const api: KonductorAPI = {
name?: string
resume?: boolean
prompt?: string
systemPrompt?: string
envScript?: string
}
) => ipcRenderer.invoke('create-session', cwd, opts),
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useCallback, useEffect, useRef } from 'react'
import type { ViewMode, Session } from './types'
import type { IssueInfo } from '../../shared/types'
import { useSessions } from './hooks/useSessions'
import { useTerminals } from './hooks/useTerminals'
import { useFileChanges } from './hooks/useFileChanges'
Expand Down Expand Up @@ -82,7 +83,13 @@ function App(): React.JSX.Element {
}, [])

const handleOpenBranchSession = useCallback(
async (branch: string, isNew: boolean, prompt?: string) => {
async (
branch: string,
isNew: boolean,
prompt?: string,
systemPrompt?: string,
issue?: IssueInfo
) => {
if (!activeProject || !activeProjectId) return

// 1. Check if a session already exists on this branch (by matching title)
Expand All @@ -100,7 +107,7 @@ function App(): React.JSX.Element {
const worktrees = await window.konductorAPI.listWorktrees(activeProject.cwd)
const wt = worktrees.find((w) => w.branch === branch)
if (wt) {
await createSession(activeProjectId, wt.path, branch, prompt)
await createSession(activeProjectId, wt.path, branch, prompt, systemPrompt, issue)
setViewMode('focus')
return
}
Expand All @@ -111,7 +118,7 @@ function App(): React.JSX.Element {
// 3. Create a new worktree and session
try {
const wt = await window.konductorAPI.createWorktree(activeProject.cwd, branch, isNew)
await createSession(activeProjectId, wt.path, branch, prompt)
await createSession(activeProjectId, wt.path, branch, prompt, systemPrompt, issue)
setViewMode('focus')
} catch (e) {
console.error('Failed to create worktree session:', e)
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/src/components/FocusView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ export default function FocusView({
<span className="text-sm text-gray-500">{projectName}</span>
<span className="text-sm text-gray-500">/</span>
<span className="text-sm text-gray-300">{session.title}</span>
{session.issue && (
<button
onClick={() => window.konductorAPI.openExternal(session.issue!.url)}
className="text-xs shrink-0 hover:underline text-blue-400"
title={`Issue #${session.issue.number}`}
>
Issue #{session.issue.number}
</button>
)}
{session.pr && session.pr.state !== 'none' && (
<button
onClick={() => window.konductorAPI.openExternal(session.pr!.url)}
Expand Down
39 changes: 32 additions & 7 deletions src/renderer/src/components/GitHubView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'
import type { Project } from '../types'
import type { GitHubPR, GitHubIssue } from '../../../shared/types'
import type { GitHubPR, GitHubIssue, IssueInfo } from '../../../shared/types'
import { ChevronLeftIcon, RefreshIcon } from './Icons'

const api = window.konductorAPI
Expand All @@ -12,7 +12,13 @@ type IssueFilter = 'open' | 'closed' | 'all'
interface GitHubViewProps {
project: Project
onBack: () => void
onOpenSession: (branch: string, isNew: boolean, prompt?: string) => void
onOpenSession: (
branch: string,
isNew: boolean,
prompt?: string,
systemPrompt?: string,
issue?: IssueInfo
) => void
}

function relativeTime(iso: string): string {
Expand Down Expand Up @@ -313,7 +319,13 @@ function PRRow({
}: {
pr: GitHubPR
onOpen: (url: string) => void
onOpenSession: (branch: string, isNew: boolean, prompt?: string) => void
onOpenSession: (
branch: string,
isNew: boolean,
prompt?: string,
systemPrompt?: string,
issue?: IssueInfo
) => void
}): React.JSX.Element {
const stateColor =
pr.state === 'open'
Expand Down Expand Up @@ -407,7 +419,13 @@ function IssueRow({
}: {
issue: GitHubIssue
onOpen: (url: string) => void
onOpenSession: (branch: string, isNew: boolean, prompt?: string) => void
onOpenSession: (
branch: string,
isNew: boolean,
prompt?: string,
systemPrompt?: string,
issue?: IssueInfo
) => void
}): React.JSX.Element {
const stateColor = issue.state === 'open' ? 'text-green-400' : 'text-red-400'

Expand Down Expand Up @@ -462,9 +480,16 @@ function IssueRow({
.replace(/^-|-$/g, '')
.slice(0, 30)
const branch = `issue-${issue.number}-${slug}`
const labels = issue.labels.length > 0 ? ` Labels: ${issue.labels.join(', ')}.` : ''
const prompt = `You are working on Issue #${issue.number}: "${issue.title}" (by @${issue.author}).${labels} Help me implement a solution for this issue.`
onOpenSession(branch, true, prompt)
const labels = issue.labels.length > 0 ? `\nLabels: ${issue.labels.join(', ')}` : ''
const assignees =
issue.assignees.length > 0 ? `\nAssignees: ${issue.assignees.join(', ')}` : ''
const body = issue.body ? `\n\n${issue.body}` : ''
const systemPrompt = `# issue\nIssue #${issue.number}: ${issue.title}\nAuthor: @${issue.author}\nState: ${issue.state}\nURL: ${issue.url}${labels}${assignees}${body}`
const prompt = `Help me implement a solution for issue #${issue.number}.`
onOpenSession(branch, true, prompt, systemPrompt, {
number: issue.number,
url: issue.url
})
}}
className="text-[10px] px-2 py-1 rounded bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors"
title={`Create branch and open session for #${issue.number}`}
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/src/components/SessionTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ export default function SessionTile({
}`}
/>
<span className="text-xs text-gray-400 truncate">{session.title}</span>
{session.issue && (
<button
onClick={(e) => {
e.stopPropagation()
window.konductorAPI.openExternal(session.issue!.url)
}}
className="text-[10px] shrink-0 hover:underline text-blue-400"
title={`Issue #${session.issue.number}`}
>
#{session.issue.number}
</button>
)}
{session.pr && session.pr.state !== 'none' && (
<button
onClick={(e) => {
Expand Down
16 changes: 15 additions & 1 deletion src/renderer/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,21 @@ export default function Sidebar({
</svg>
)}
<div className="min-w-0">
<span className="text-xs truncate block">{session.title}</span>
<div className="flex items-center gap-1.5">
<span className="text-xs truncate">{session.title}</span>
{session.issue && (
<button
onClick={(e) => {
e.stopPropagation()
window.konductorAPI.openExternal(session.issue!.url)
}}
className="text-[9px] shrink-0 hover:underline text-blue-400"
title={`Issue #${session.issue.number}`}
>
#{session.issue.number}
</button>
)}
</div>
{isWorktree && (
<span className="text-[9px] text-gray-600 truncate block">
{session.cwd.split('/').slice(-3).join('/')}
Expand Down
28 changes: 21 additions & 7 deletions src/renderer/src/hooks/useSessions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { Terminal } from '@xterm/xterm'
import type { PrInfo } from '../../../shared/types'
import type { PrInfo, IssueInfo } from '../../../shared/types'
import type { Project, Session, ActivityState } from '../types'
import type { GridCols } from '../components/GridView'
import { TERM_THEME } from '../termTheme'
Expand Down Expand Up @@ -30,6 +30,7 @@ interface SessionMeta {
summary: string
claudeSessionId: string
pr?: PrInfo
issue?: IssueInfo
}

interface HmrState {
Expand Down Expand Up @@ -131,7 +132,8 @@ export function useSessions() {
claudeSessionId: meta.claudeSessionId,
activity: 'ready',
dormant: true,
pr: meta.pr
pr: meta.pr,
issue: meta.issue
}))
setSessions(dormant)
if (state.activeSessionIndex != null && state.activeSessionIndex < dormant.length) {
Expand Down Expand Up @@ -196,7 +198,8 @@ export function useSessions() {
claudeSessionId: meta.claudeSessionId,
activity: 'ready',
dormant: false,
pr: meta.pr
pr: meta.pr,
issue: meta.issue
})
}

Expand Down Expand Up @@ -228,7 +231,8 @@ export function useSessions() {
title: s.title,
summary: s.summary,
claudeSessionId: s.claudeSessionId,
pr: s.pr
pr: s.pr,
issue: s.issue
})),
activeSessionIndex: activeIdx >= 0 ? activeIdx : null,
gridCols
Expand Down Expand Up @@ -267,7 +271,8 @@ export function useSessions() {
title: s.title,
summary: s.summary,
claudeSessionId: s.claudeSessionId,
pr: s.pr
pr: s.pr,
issue: s.issue
})),
activeSessionId: r.activeSessionId(),
gridCols: r.gridCols()
Expand Down Expand Up @@ -442,14 +447,22 @@ export function useSessions() {
}, [])

const createSession = useCallback(
async (projectId: string, cwd: string, branch?: string, prompt?: string) => {
async (
projectId: string,
cwd: string,
branch?: string,
prompt?: string,
systemPrompt?: string,
issue?: IssueInfo
) => {
const sessionCount = sessionsRef.current.filter((s) => s.projectId === projectId).length
const title = branch ? `${branch}` : `Session ${sessionCount + 1}`
const project = projects.find((p) => p.id === projectId)

const { id, claudeSessionId } = await api.createSession(cwd, {
name: title,
prompt,
systemPrompt,
envScript: project?.envScript
})

Expand All @@ -468,7 +481,8 @@ export function useSessions() {
alive: true,
claudeSessionId,
activity: 'ready',
dormant: false
dormant: false,
issue
}

setSessions((prev) => [...prev, session])
Expand Down
Loading
Loading