feat: show file change summary bar (#950)
* feat: add FileChangeSummary component for multi-file diff preview in ToolPart Provides an aggregated diff card for apply_patch and multi-edit tools, showing per-file stats with click-to-expand diff view. * feat: add PendingChangesBar above chat input with collapse/expand and file opening - Collapsed/expanded toggle with aggregate +N -N stats (green/red) - Relative path display, chat-column alignment with ChatInput - Click file to open in diff viewer (web/desktop) or editor (VS Code) - Support edit/multiedit/apply_patch/write tool metadata extraction - Add PendingChangesBar to main chat view (ChatContainer) * feat: dual-mode ChangedFilesBar — Git diff state vs latest AI turn Git mode: reads git status from useGitStore (status.files + diffStats), auto-clears on commit/restore. Shows 'N files changed in workspace'. Non-Git mode: latest assistant turn only (no accumulation), clears on new user message or manual dismiss. Shows 'AI updated N files in the last reply'. Both modes: dismiss button with signature-based tracking. Add pendingChangesBarDismissed state to session-ui-store, cleared on sendMessage. * fix: address code review issues in ChangedFilesBar - Gate non-git mode on streaming state to prevent flicker during AI turns - Return null when isGitRepo is unknown (loading state) - Add group/row class for reject button visibility in FileChangeSummary - Use per-session Map for dismiss tracking to prevent cross-session leaks - Include additions/deletions in dismiss signature for re-edit detection - Extract shared parsePatchStats/parseCount to fileChangeHelpers * chore: revert local .opencode/package-lock.json changes from PR * fix: per-part fallback guard and git-only reject button - Fix extractChangedFiles to use per-part files.length snapshot instead of global guard, preventing file entries from being skipped when earlier parts already contributed files - Hide reject button in FileChangeSummary when not in a git repo, preventing silent revert failures * refactor: remove FileChangeSummary — dead code redundant with ToolPart FileChangeSummary duplicated diff rendering that ToolPart already provides (PatchDiff, per-file stats, DiffViewToggle). The only unique feature was a git revert button, which conflicts with the design principle of not having accept/reject on file change previews. Moved parsePatchStats/parseCount back into PendingChangesBar (sole consumer) and deleted the shared helper module. * chore: remove stray Tester.txt * fix: deduplicate Fallback 4 'Diff' placeholder via seen set * chore: update non-Git mode copy to neutral 'changed in the last reply' * fix(chat): unify changes row with tasks --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d73edc672e
commit
d4a4f43a83
@@ -187,6 +187,10 @@ export type SessionUIState = {
|
||||
markSessionPlanAvailable: (sessionId: string) => void
|
||||
isSessionPlanAvailable: (sessionId: string) => boolean
|
||||
|
||||
// Non-Git mode: dismissed signature hash per session, hides bar until new turn arrives
|
||||
pendingChangesBarDismissed: Map<string, string>
|
||||
dismissPendingChangesBar: (sessionId: string, signature: string | null) => void
|
||||
|
||||
// Actions — UI state management
|
||||
setCurrentSession: (id: string | null, directoryHint?: string | null) => void
|
||||
openNewSessionDraft: (options?: Partial<NewSessionDraftState>) => void
|
||||
@@ -343,6 +347,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
isLoading: false,
|
||||
lastLoadedDirectory: null,
|
||||
sessionPlanAvailable: new Map(),
|
||||
pendingChangesBarDismissed: new Map(),
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setCurrentSession
|
||||
@@ -650,6 +655,16 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
|
||||
getWorktreeMetadata: (sessionId) => get().worktreeMetadata.get(sessionId),
|
||||
|
||||
dismissPendingChangesBar: (sessionId, signature) => {
|
||||
const map = new Map(get().pendingChangesBarDismissed);
|
||||
if (signature === null) {
|
||||
map.delete(sessionId);
|
||||
} else {
|
||||
map.set(sessionId, signature);
|
||||
}
|
||||
set({ pendingChangesBarDismissed: map });
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sendMessage — calls SDK, reads domain data from sync
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -664,6 +679,14 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
variant?: string,
|
||||
inputMode?: "normal" | "shell",
|
||||
) => {
|
||||
// Clear non-Git changed-files bar on new user message for current session
|
||||
const sid = get().currentSessionId;
|
||||
if (sid) {
|
||||
const map = new Map(get().pendingChangesBarDismissed);
|
||||
map.delete(sid);
|
||||
set({ pendingChangesBarDismissed: map });
|
||||
}
|
||||
|
||||
const draft = get().newSessionDraft
|
||||
const trimmedAgent = typeof agent === "string" && agent.trim().length > 0 ? agent.trim() : undefined
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { syncDebug } from "./debug"
|
||||
import { opencodeClient } from "@/lib/opencode/client"
|
||||
import { usePermissionStore } from "@/stores/permissionStore"
|
||||
import { useConfigStore } from "@/stores/useConfigStore"
|
||||
import { useTodosPersistStore } from "@/stores/useTodosPersistStore"
|
||||
import { toast } from "@/components/ui"
|
||||
import { appendNotification } from "./notification-store"
|
||||
import type { State } from "./types"
|
||||
@@ -1159,7 +1160,11 @@ function handleEvent(
|
||||
break
|
||||
}
|
||||
|
||||
if (applyDirectoryEvent(draft, payload)) {
|
||||
if (applyDirectoryEvent(draft, payload, {
|
||||
onSetSessionTodo: (sessionID, todos) => {
|
||||
useTodosPersistStore.getState().setSessionTodos(sessionID, todos)
|
||||
},
|
||||
})) {
|
||||
store.setState(draft)
|
||||
const sessionID = getSessionIdFromPayload(payload) ?? undefined
|
||||
const messageID = getMessageIdFromPayload(payload) ?? undefined
|
||||
|
||||
Reference in New Issue
Block a user