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
@@ -0,0 +1,64 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import type { Todo } from '@opencode-ai/sdk/v2/client';
|
||||
import { getSafeStorage } from './utils/safeStorage';
|
||||
|
||||
const MAX_SESSIONS = 50;
|
||||
|
||||
interface SessionTodosRecord {
|
||||
todos: Todo[];
|
||||
touchedAt: number;
|
||||
}
|
||||
|
||||
interface TodosPersistState {
|
||||
sessions: Record<string, SessionTodosRecord>;
|
||||
setSessionTodos: (sessionId: string, todos: Todo[] | undefined) => void;
|
||||
getSessionTodos: (sessionId: string) => Todo[] | undefined;
|
||||
}
|
||||
|
||||
const evictOldest = (sessions: Record<string, SessionTodosRecord>): Record<string, SessionTodosRecord> => {
|
||||
const ids = Object.keys(sessions);
|
||||
if (ids.length <= MAX_SESSIONS) return sessions;
|
||||
|
||||
const sorted = ids
|
||||
.map((id) => [id, sessions[id].touchedAt] as const)
|
||||
.sort((a, b) => a[1] - b[1]);
|
||||
const drop = sorted.slice(0, ids.length - MAX_SESSIONS).map(([id]) => id);
|
||||
const next = { ...sessions };
|
||||
for (const id of drop) delete next[id];
|
||||
return next;
|
||||
};
|
||||
|
||||
export const useTodosPersistStore = create<TodosPersistState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
sessions: {},
|
||||
setSessionTodos: (sessionId, todos) => {
|
||||
if (!sessionId) return;
|
||||
set((state) => {
|
||||
const next = { ...state.sessions };
|
||||
if (!todos || todos.length === 0) {
|
||||
if (!(sessionId in next)) return state;
|
||||
delete next[sessionId];
|
||||
return { sessions: next };
|
||||
}
|
||||
next[sessionId] = { todos, touchedAt: Date.now() };
|
||||
return { sessions: evictOldest(next) };
|
||||
});
|
||||
},
|
||||
getSessionTodos: (sessionId) => {
|
||||
if (!sessionId) return undefined;
|
||||
return get().sessions[sessionId]?.todos;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'openchamber-session-todos',
|
||||
version: 1,
|
||||
storage: createJSONStorage(() => getSafeStorage()),
|
||||
partialize: (state) => ({ sessions: state.sessions }),
|
||||
},
|
||||
),
|
||||
{ name: 'TodosPersistStore' },
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user