Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
676d083
Refine header git commit browser
friuns May 10, 2026
c95db85
Add commit detail panel to git dropdown
friuns May 11, 2026
ccbb37f
Fix header dropdown stacking
friuns May 11, 2026
cd44267
Hide commit files until selection
friuns May 11, 2026
ff74f65
Move commit files panel left
friuns May 11, 2026
1a51cc5
Show commit file line counts
friuns May 11, 2026
ff97332
Copy commit refs from dropdown
friuns May 11, 2026
ab5f544
Document persistent verification dev server
friuns May 11, 2026
0374a08
Keep mobile review close visible
friuns May 11, 2026
c22ebfd
Close git dropdown when toggling review
friuns May 11, 2026
2f1f105
Remove run review button
friuns May 11, 2026
3b9f424
Remove findings from review pane
friuns May 11, 2026
8686611
Open commit files in review pane
friuns May 11, 2026
7c90e8f
Stop centering review hunk on file select
friuns May 11, 2026
1ba849b
Show worktree change counts in git dropdown
friuns May 11, 2026
eeba23e
Layer review pane above git dropdown
friuns May 11, 2026
4c356a4
Improve mobile git dropdown behavior
friuns May 11, 2026
78647e7
Fix narrow review file names
friuns May 11, 2026
f0a5329
Remove review file row indentation
friuns May 11, 2026
36d02fd
Fix mobile review pane scrolling
friuns May 11, 2026
1885f1d
Reorder mobile git dropdown panels
friuns May 11, 2026
7a9355e
Fix reset history commit reload dedupe
friuns May 12, 2026
374c8c2
Fix Qodo review findings
friuns May 12, 2026
88653f3
Fix git dropdown review follow-ups
friuns May 12, 2026
9c42465
Fix commit review diff follow-ups
friuns May 12, 2026
7c8313e
Strengthen review pane dark overrides
friuns May 12, 2026
9cad38f
Preserve exact git paths in review APIs
friuns May 12, 2026
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
5. For responsive/mobile changes, run checks at 375x812 and 768x1024.
6. Wait 2-3 seconds before capturing final screenshot(s).
7. Save screenshots under `output/playwright/` with task-specific names.
8. Leave the dev server running after verification unless the user explicitly asks to stop it.
- Capture screenshots only when Playwright verification is requested.
- If the dev server fails to start due to pre-existing errors, fix them first or work around them before testing.
- If requested Playwright assertions fail, do not report completion; fix and re-run until passing.
Expand Down Expand Up @@ -152,6 +153,7 @@
- For dev-server fixes, verify the exact user-requested command afterwards (for example `npm run dev`), not only a fallback Vite invocation.
- Never kill or stop the tmux-managed dev server bound to port `5173`.
- Treat the `5173` tmux dev process as persistent infrastructure; restart it only when the user explicitly requests a restart.
- Treat the `4173` verification dev server as reusable test infrastructure during active UI work; after tests or screenshots, leave it running unless the user explicitly asks to stop it.

## Dark Theme CSS Rule

Expand Down
119 changes: 109 additions & 10 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -541,19 +541,25 @@
:head-date="currentThreadHeadDate"
:detached="isThreadDetachedHead"
:dirty="isThreadWorktreeDirty"
:worktree-change-summary="threadWorktreeChangeSummary"
:branches="threadBranchOptions"
:commits-by-branch="threadBranchCommitsByBranch"
:commits-loading-for="threadBranchCommitsLoadingFor"
:commits-error="threadBranchCommitsError"
:commit-files-by-sha="threadCommitFilesBySha"
:commit-files-loading-for="threadCommitFilesLoadingFor"
:commit-files-error="threadCommitFilesError"
:loading="isLoadingThreadBranches"
:busy="isSwitchingThreadBranch"
:error="threadBranchError"
:review-open="isReviewPaneOpen"
:show-review="route.name === 'thread' && selectedThreadId.length > 0"
@toggle-review="isReviewPaneOpen = !isReviewPaneOpen"
@toggle-review="onToggleContentHeaderReview"
@checkout-branch="onCheckoutContentHeaderBranch"
@reset-branch-to-commit="onResetContentHeaderBranchToCommit"
@load-commits="loadThreadBranchCommits"
@load-commit-files="loadThreadCommitFiles"
@open-commit-file="onOpenContentHeaderCommitFile"
/>
</template>
</ContentHeader>
Expand Down Expand Up @@ -920,6 +926,8 @@
:thread-id="selectedThreadId"
:cwd="composerCwd"
:is-thread-in-progress="isSelectedThreadInProgress"
:initial-file-path="reviewInitialFilePath"
:commit-sha="reviewInitialCommitSha"
@close="isReviewPaneOpen = false"
/>

Expand Down Expand Up @@ -1097,7 +1105,9 @@ import {
createProjectlessThreadDirectory,
getGitBranchState,
getGitBranchCommits,
getGitCommitFiles,
getGitRepositoryStatus,
getReviewSummary,
getWorktreeBranchOptions,
getAccounts,
completeCodexLogin,
Expand All @@ -1122,7 +1132,7 @@ import {
} from './api/codexGateway'
import type { ReasoningEffort, SpeedMode, UiAccountEntry, UiRateLimitWindow, UiServerRequest, UiServerRequestReply, UiThreadAutomation, UiThreadTokenUsage } from './types/codex'
import type { ComposerDraftPayload, ThreadComposerExposed } from './components/content/ThreadComposer.vue'
import type { GitCommitOption, LocalDirectoryEntry, TelegramStatus, ThreadTerminalQuickCommand, WorktreeBranchOption } from './api/codexGateway'
import type { GitCommitFileChange, GitCommitOption, LocalDirectoryEntry, TelegramStatus, ThreadTerminalQuickCommand, WorktreeBranchOption } from './api/codexGateway'
import { getFreeModeStatus, setFreeMode, setFreeModeCustomKey, setCustomProvider } from './api/codexGateway'
import { getPathLeafName, getPathParent, isProjectlessChatPath, normalizePathForUi } from './pathUtils.js'

Expand Down Expand Up @@ -1426,24 +1436,37 @@ let threadSearchTimer: ReturnType<typeof setTimeout> | null = null
let terminalKeyboardFocusFallbackTimer: ReturnType<typeof setTimeout> | null = null
let threadBranchesRequestId = 0
let threadBranchCommitsRequestId = 0
let threadCommitFilesRequestId = 0
let threadWorktreeSummaryRequestId = 0
const defaultNewProjectName = ref('New Project (1)')
const homeDirectory = ref('')
const isSettingsOpen = ref(false)
const isAccountsSectionCollapsed = ref(loadAccountsSectionCollapsed())
const isReviewPaneOpen = ref(false)
const reviewInitialFilePath = ref('')
const reviewInitialCommitSha = ref('')
const threadBranchOptions = ref<WorktreeBranchOption[]>([])
const currentThreadBranch = ref<string | null>(null)
const currentThreadHeadSha = ref<string | null>(null)
const currentThreadHeadSubject = ref<string | null>(null)
const currentThreadHeadDate = ref<string | null>(null)
const isThreadDetachedHead = ref(false)
const isThreadWorktreeDirty = ref(false)
const threadWorktreeChangeSummary = ref({ addedLineCount: 0, removedLineCount: 0 })
const threadBranchError = ref('')
const threadBranchCommitsByBranch = ref<Record<string, GitCommitOption[]>>({})
const threadBranchCommitsLoadingFor = ref('')
const threadBranchCommitsError = ref('')
const threadCommitFilesBySha = ref<Record<string, GitCommitFileChange[]>>({})
const threadCommitFilesLoadingFor = ref('')
const threadCommitFilesError = ref('')
const isLoadingThreadBranches = ref(false)
const isSwitchingThreadBranch = ref(false)

function toThreadBranchCommitsKey(branch: string, includeResetHistory: boolean): string {
return `${branch}\u0000${includeResetHistory ? 'with-reset-history' : 'without-reset-history'}`
}

const createFolderInputRef = ref<HTMLInputElement | null>(null)
const accounts = ref<UiAccountEntry[]>([])
const isRefreshingAccounts = ref(false)
Expand Down Expand Up @@ -3085,20 +3108,47 @@ function canLoadBranchStateForCwd(cwd: string): boolean {
function resetThreadBranchState(): void {
threadBranchesRequestId += 1
threadBranchCommitsRequestId += 1
threadCommitFilesRequestId += 1
threadWorktreeSummaryRequestId += 1
threadBranchOptions.value = []
currentThreadBranch.value = null
currentThreadHeadSha.value = null
currentThreadHeadSubject.value = null
currentThreadHeadDate.value = null
isThreadDetachedHead.value = false
isThreadWorktreeDirty.value = false
threadWorktreeChangeSummary.value = { addedLineCount: 0, removedLineCount: 0 }
threadBranchCommitsByBranch.value = {}
threadBranchCommitsLoadingFor.value = ''
threadBranchCommitsError.value = ''
threadCommitFilesBySha.value = {}
threadCommitFilesLoadingFor.value = ''
threadCommitFilesError.value = ''
threadBranchError.value = ''
isLoadingThreadBranches.value = false
}

function loadThreadWorktreeChangeSummary(cwd: string): void {
const targetCwd = cwd.trim()
if (!targetCwd) {
threadWorktreeChangeSummary.value = { addedLineCount: 0, removedLineCount: 0 }
return
}
const requestId = ++threadWorktreeSummaryRequestId
void getReviewSummary(targetCwd, 'unstaged')
.then((summary) => {
if (requestId !== threadWorktreeSummaryRequestId || !canLoadBranchStateForCwd(targetCwd)) return
threadWorktreeChangeSummary.value = {
addedLineCount: summary.addedLineCount,
removedLineCount: summary.removedLineCount,
}
})
.catch(() => {
if (requestId !== threadWorktreeSummaryRequestId || !canLoadBranchStateForCwd(targetCwd)) return
threadWorktreeChangeSummary.value = { addedLineCount: 0, removedLineCount: 0 }
})
}

async function loadThreadBranches(cwd: string): Promise<void> {
const targetCwd = cwd.trim()
if (!targetCwd) {
Expand All @@ -3118,6 +3168,9 @@ async function loadThreadBranches(cwd: string): Promise<void> {
currentThreadHeadDate.value = state.headDate
isThreadDetachedHead.value = state.detached
isThreadWorktreeDirty.value = state.dirty
loadThreadWorktreeChangeSummary(targetCwd)
const defaultBranchForCommits = state.currentBranch?.trim() || state.options[0]?.value?.trim() || ''
if (defaultBranchForCommits) loadThreadBranchCommits({ branch: defaultBranchForCommits, includeResetHistory: true })
} catch {
if (requestId !== threadBranchesRequestId || !canLoadBranchStateForCwd(targetCwd)) return
threadBranchOptions.value = []
Expand All @@ -3127,6 +3180,7 @@ async function loadThreadBranches(cwd: string): Promise<void> {
currentThreadHeadDate.value = null
isThreadDetachedHead.value = false
isThreadWorktreeDirty.value = false
threadWorktreeChangeSummary.value = { addedLineCount: 0, removedLineCount: 0 }
} finally {
if (requestId === threadBranchesRequestId) {
isLoadingThreadBranches.value = false
Expand All @@ -3141,6 +3195,7 @@ function applyThreadGitState(state: { currentBranch: string | null; headSha: str
currentThreadHeadDate.value = state.headDate
isThreadDetachedHead.value = state.detached
isThreadWorktreeDirty.value = state.dirty
loadThreadWorktreeChangeSummary(composerCwd.value)
}

function onCheckoutContentHeaderBranch(value: string): void {
Expand Down Expand Up @@ -3198,33 +3253,77 @@ function onResetContentHeaderBranchToCommit(payload: { branch: string; sha: stri
})
}

function loadThreadBranchCommits(branch: string): void {
const targetBranch = branch.trim()
function loadThreadBranchCommits(payload: string | { branch: string; includeResetHistory?: boolean }): void {
const targetBranch = (typeof payload === 'string' ? payload : payload.branch).trim()
const includeResetHistory = typeof payload === 'string' ? true : payload.includeResetHistory !== false
const cwd = composerCwd.value.trim()
if (!targetBranch || !cwd || threadBranchCommitsLoadingFor.value === targetBranch) return
if (threadBranchCommitsByBranch.value[targetBranch]) return
const cacheKey = toThreadBranchCommitsKey(targetBranch, includeResetHistory)
if (!targetBranch || !cwd || threadBranchCommitsLoadingFor.value === cacheKey) return
if (threadBranchCommitsByBranch.value[cacheKey]) return
const requestId = ++threadBranchCommitsRequestId
threadBranchCommitsLoadingFor.value = targetBranch
threadBranchCommitsLoadingFor.value = cacheKey
threadBranchCommitsError.value = ''
void getGitBranchCommits(cwd, targetBranch)
void getGitBranchCommits(cwd, targetBranch, { includeResetHistory })
.then((commits) => {
if (requestId !== threadBranchCommitsRequestId || !canLoadBranchStateForCwd(cwd)) return
threadBranchCommitsByBranch.value = {
...threadBranchCommitsByBranch.value,
[targetBranch]: commits,
[cacheKey]: commits,
}
})
.catch((error: unknown) => {
if (requestId !== threadBranchCommitsRequestId || !canLoadBranchStateForCwd(cwd)) return
threadBranchCommitsError.value = error instanceof Error ? error.message : 'Failed to load branch commits'
})
.finally(() => {
if (requestId === threadBranchCommitsRequestId && threadBranchCommitsLoadingFor.value === targetBranch) {
if (requestId === threadBranchCommitsRequestId && threadBranchCommitsLoadingFor.value === cacheKey) {
threadBranchCommitsLoadingFor.value = ''
}
})
}

function loadThreadCommitFiles(sha: string): void {
const targetSha = sha.trim()
const cwd = composerCwd.value.trim()
if (!targetSha || !cwd || threadCommitFilesLoadingFor.value === targetSha) return
if (threadCommitFilesBySha.value[targetSha]) return
const requestId = ++threadCommitFilesRequestId
threadCommitFilesLoadingFor.value = targetSha
threadCommitFilesError.value = ''
void getGitCommitFiles(cwd, targetSha)
.then((files) => {
if (requestId !== threadCommitFilesRequestId || !canLoadBranchStateForCwd(cwd)) return
threadCommitFilesBySha.value = {
...threadCommitFilesBySha.value,
[targetSha]: files,
}
})
.catch((error: unknown) => {
if (requestId !== threadCommitFilesRequestId || !canLoadBranchStateForCwd(cwd)) return
threadCommitFilesError.value = error instanceof Error ? error.message : 'Failed to load commit files'
})
.finally(() => {
if (requestId === threadCommitFilesRequestId && threadCommitFilesLoadingFor.value === targetSha) {
threadCommitFilesLoadingFor.value = ''
}
})
}

function onOpenContentHeaderCommitFile(payload: { sha: string; path: string }): void {
const targetPath = payload.path.trim()
const targetSha = payload.sha.trim()
if (!targetPath || !targetSha) return
reviewInitialFilePath.value = targetPath
reviewInitialCommitSha.value = targetSha
isReviewPaneOpen.value = true
}

function onToggleContentHeaderReview(): void {
reviewInitialFilePath.value = ''
reviewInitialCommitSha.value = ''
isReviewPaneOpen.value = !isReviewPaneOpen.value
}

async function onOpenProjectSetupModal(): Promise<void> {
const baseDir = await resolveProjectBaseDirectory()
if (!baseDir) return
Expand Down
Loading