diff --git a/src/main/__tests__/github.test.ts b/src/main/__tests__/github.test.ts index 6c26385..f205a31 100644 --- a/src/main/__tests__/github.test.ts +++ b/src/main/__tests__/github.test.ts @@ -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' }], @@ -193,6 +194,7 @@ describe('listIssues', () => { { number: 11, title: 'Done issue', + body: '', state: 'CLOSED', author: { login: 'charlie' }, labels: [], @@ -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([]) }) diff --git a/src/main/github.ts b/src/main/github.ts index 766c5f4..c1eea84 100644 --- a/src/main/github.ts +++ b/src/main/github.ts @@ -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( @@ -110,6 +110,7 @@ export async function listIssues( (issue: { number: number title: string + body: string state: string author: { login: string } labels: { name: string }[] @@ -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), diff --git a/src/main/sessionManager.ts b/src/main/sessionManager.ts index 9f3dc23..4c16c69 100644 --- a/src/main/sessionManager.ts +++ b/src/main/sessionManager.ts @@ -134,6 +134,7 @@ function spawnClaude( name: string, resume: boolean, prompt?: string, + systemPrompt?: string, env?: Record ): nodePty.IPty { const args = resume @@ -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}`) @@ -169,6 +175,7 @@ export function createSession( name?: string resume?: boolean prompt?: string + systemPrompt?: string envScript?: string } ): { id: string; claudeSessionId: string } { @@ -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, diff --git a/src/main/store.ts b/src/main/store.ts index 653009f..9f8eaf7 100644 --- a/src/main/store.ts +++ b/src/main/store.ts @@ -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') @@ -20,6 +20,7 @@ export interface SessionData { summary: string claudeSessionId: string pr?: PrInfo + issue?: IssueInfo } export interface PersistedState { diff --git a/src/preload/index.ts b/src/preload/index.ts index f15f432..759db39 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -29,6 +29,7 @@ export interface KonductorAPI { name?: string resume?: boolean prompt?: string + systemPrompt?: string envScript?: string } ) => Promise<{ id: string; claudeSessionId: string }> @@ -103,6 +104,7 @@ const api: KonductorAPI = { name?: string resume?: boolean prompt?: string + systemPrompt?: string envScript?: string } ) => ipcRenderer.invoke('create-session', cwd, opts), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7f3bccc..29af8a7 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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' @@ -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) @@ -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 } @@ -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) diff --git a/src/renderer/src/components/FocusView.tsx b/src/renderer/src/components/FocusView.tsx index 1dec505..033f3b8 100644 --- a/src/renderer/src/components/FocusView.tsx +++ b/src/renderer/src/components/FocusView.tsx @@ -85,6 +85,15 @@ export default function FocusView({ {projectName} / {session.title} + {session.issue && ( + + )} {session.pr && session.pr.state !== 'none' && ( + )} {session.pr && session.pr.state !== 'none' && ( + )} + {isWorktree && ( {session.cwd.split('/').slice(-3).join('/')} diff --git a/src/renderer/src/hooks/useSessions.ts b/src/renderer/src/hooks/useSessions.ts index 730db17..901fe48 100644 --- a/src/renderer/src/hooks/useSessions.ts +++ b/src/renderer/src/hooks/useSessions.ts @@ -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' @@ -30,6 +30,7 @@ interface SessionMeta { summary: string claudeSessionId: string pr?: PrInfo + issue?: IssueInfo } interface HmrState { @@ -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) { @@ -196,7 +198,8 @@ export function useSessions() { claudeSessionId: meta.claudeSessionId, activity: 'ready', dormant: false, - pr: meta.pr + pr: meta.pr, + issue: meta.issue }) } @@ -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 @@ -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() @@ -442,7 +447,14 @@ 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) @@ -450,6 +462,7 @@ export function useSessions() { const { id, claudeSessionId } = await api.createSession(cwd, { name: title, prompt, + systemPrompt, envScript: project?.envScript }) @@ -468,7 +481,8 @@ export function useSessions() { alive: true, claudeSessionId, activity: 'ready', - dormant: false + dormant: false, + issue } setSessions((prev) => [...prev, session]) diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index c60769c..bb07afd 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import type { PrInfo } from '../../shared/types' +import type { PrInfo, IssueInfo } from '../../shared/types' export type ViewMode = 'grid' | 'focus' | 'branches' | 'github' | 'settings' @@ -26,6 +26,8 @@ export interface Session { dormant: boolean /** PR associated with the session's branch (if any) */ pr?: PrInfo + /** GitHub issue this session was created from (if any) */ + issue?: IssueInfo } export interface ShellTerminal { diff --git a/src/shared/types.ts b/src/shared/types.ts index 543a165..d3a6d62 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -23,6 +23,7 @@ export interface GitHubPR { export interface GitHubIssue { number: number title: string + body: string state: 'open' | 'closed' author: string labels: string[] @@ -48,6 +49,11 @@ export interface PrInfo { url: string } +export interface IssueInfo { + number: number + url: string +} + export interface BranchDetail { name: string isHead: boolean