* feat: add view ready and destroy callbacks to CodeMirrorEditor Provide onViewReady callback that receives the EditorView once created Provide onViewDestroy callback invoked when the editor is destroyed * feat: render inline drafts in PlanView with portal Add editorView state to track the active CodeMirror instance Render inline drafts through a portal anchored to the editor scroll container Position drafts relative to the corresponding start line in the editor * feat(PierreDiffViewer): enable editing inline diff drafts Enable editing of existing inline diff drafts with draftID handling Auto-focus comment input when opening editor and place cursor at end * feat: enable editing inline drafts and auto focus Enable editing of existing inline drafts via new editingDraftId state and updateDraft Auto focus the comment input and place cursor at end when a line is selected Add edit action button in the inline actions bar to start editing * feat: add updateDraft to inline comment drafts store Add updateDraft action to modify a specific draft by session and id Merge updates into the target draft without altering other fields Retains existing draft management actions like remove and clear
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
import { create } from 'zustand';
|
|
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
|
|
import { getSafeStorage } from './utils/safeStorage';
|
|
|
|
export type InlineCommentSource = 'diff' | 'plan' | 'file';
|
|
|
|
export interface InlineCommentDraft {
|
|
id: string;
|
|
sessionKey: string; // sessionId or 'draft' for new sessions
|
|
source: InlineCommentSource;
|
|
fileLabel: string; // filename or 'plan'
|
|
startLine: number;
|
|
endLine: number;
|
|
side?: 'original' | 'modified'; // diff only
|
|
code: string;
|
|
language: string;
|
|
text: string;
|
|
createdAt: number;
|
|
}
|
|
|
|
interface InlineCommentDraftState {
|
|
drafts: Record<string, InlineCommentDraft[]>; // sessionKey -> drafts
|
|
}
|
|
|
|
interface InlineCommentDraftActions {
|
|
addDraft: (draft: Omit<InlineCommentDraft, 'id' | 'createdAt'>) => void;
|
|
updateDraft: (sessionKey: string, draftId: string, updates: Partial<Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>>) => void;
|
|
removeDraft: (sessionKey: string, draftId: string) => void;
|
|
clearDrafts: (sessionKey: string) => void;
|
|
getDrafts: (sessionKey: string) => InlineCommentDraft[];
|
|
consumeDrafts: (sessionKey: string) => InlineCommentDraft[];
|
|
getDraftCount: (sessionKey: string) => number;
|
|
hasDrafts: (sessionKey: string) => boolean;
|
|
}
|
|
|
|
type InlineCommentDraftStore = InlineCommentDraftState & InlineCommentDraftActions;
|
|
|
|
export const useInlineCommentDraftStore = create<InlineCommentDraftStore>()(
|
|
devtools(
|
|
persist(
|
|
(set, get) => ({
|
|
drafts: {},
|
|
|
|
addDraft: (draft) => {
|
|
const id = `icd-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
const newDraft: InlineCommentDraft = {
|
|
...draft,
|
|
id,
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
set((state) => {
|
|
const currentDrafts = state.drafts[draft.sessionKey] ?? [];
|
|
return {
|
|
drafts: {
|
|
...state.drafts,
|
|
[draft.sessionKey]: [...currentDrafts, newDraft],
|
|
},
|
|
};
|
|
});
|
|
|
|
return id;
|
|
},
|
|
|
|
updateDraft: (sessionKey, draftId, updates) => {
|
|
set((state) => {
|
|
const currentDrafts = state.drafts[sessionKey] ?? [];
|
|
const newDrafts = currentDrafts.map((draft) => {
|
|
if (draft.id !== draftId) {
|
|
return draft;
|
|
}
|
|
return {
|
|
...draft,
|
|
...updates,
|
|
};
|
|
});
|
|
|
|
return {
|
|
drafts: {
|
|
...state.drafts,
|
|
[sessionKey]: newDrafts,
|
|
},
|
|
};
|
|
});
|
|
},
|
|
|
|
removeDraft: (sessionKey, draftId) => {
|
|
set((state) => {
|
|
const currentDrafts = state.drafts[sessionKey] ?? [];
|
|
const newDrafts = currentDrafts.filter((d) => d.id !== draftId);
|
|
|
|
if (newDrafts.length === 0) {
|
|
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
|
void _removed;
|
|
return { drafts: rest };
|
|
}
|
|
|
|
return {
|
|
drafts: {
|
|
...state.drafts,
|
|
[sessionKey]: newDrafts,
|
|
},
|
|
};
|
|
});
|
|
},
|
|
|
|
clearDrafts: (sessionKey) => {
|
|
set((state) => {
|
|
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
|
void _removed;
|
|
return { drafts: rest };
|
|
});
|
|
},
|
|
|
|
getDrafts: (sessionKey) => {
|
|
return get().drafts[sessionKey] ?? [];
|
|
},
|
|
|
|
consumeDrafts: (sessionKey) => {
|
|
const drafts = get().drafts[sessionKey] ?? [];
|
|
if (drafts.length === 0) return [];
|
|
|
|
// Sort by creation time to maintain order
|
|
const sortedDrafts = [...drafts].sort((a, b) => a.createdAt - b.createdAt);
|
|
|
|
// Clear drafts after consuming
|
|
set((state) => {
|
|
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
|
void _removed;
|
|
return { drafts: rest };
|
|
});
|
|
|
|
return sortedDrafts;
|
|
},
|
|
|
|
getDraftCount: (sessionKey) => {
|
|
return (get().drafts[sessionKey] ?? []).length;
|
|
},
|
|
|
|
hasDrafts: (sessionKey) => {
|
|
return (get().drafts[sessionKey] ?? []).length > 0;
|
|
},
|
|
}),
|
|
{
|
|
name: 'openchamber-inline-comment-drafts',
|
|
storage: createJSONStorage(() => getSafeStorage()),
|
|
}
|
|
),
|
|
{ name: 'inline-comment-draft-store' }
|
|
)
|
|
);
|
|
|
|
export default useInlineCommentDraftStore;
|