Introduce inline comment in plan and diff panel (#277)
* feat(chat): support inline comment drafts in chat input Add per-session inline comment draft tracking Append inline drafts to outgoing messages when queuing Consume drafts after sending to clear state * feat: add actions menu to PierreDiffViewer Add a dropdown actions menu in the diff viewer with More and Delete options Enable inline comment drafting support and selection synchronization to prevent loops Replace previous action icon with contextual actions in the header * feat(plan): enable inline comment drafting in PlanView Add inline comment draft support with addDraft, removeDraft and drafts state Compute session key for drafts based on currentSessionId and draft state Prepare dropdown and toast utilities for draft actions in PlanView * feat: add inline comment formatting utilities Introduce formatInlineCommentDraft to render inline comments for diff and plan views Add appendInlineComments to attach comments to existing messages with proper separation Provide hasInlineComments to detect inline comment blocks in text * feat: add inline comment draft store Persist drafts per session key for inline comments Add, remove, and clear drafts with unique IDs per session Consume drafts to move them out of store after submission
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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;
|
||||
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;
|
||||
},
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user