feat(chat): Comments and review in VS Code, like the OpenChamber desktop app (#1724)
* feat(chat): render code comments as cards instead of fenced text * fix(vscode): route Add Comment to the active session editor panel * fix(chat): persist queued inline comments and tighten file-chip path matching * feat(vscode): comment on code from the editor * fix(chat): keep attached context in the message and broadcast comment removal * fix(vscode): hold every pending comment and gate both entry points on the workspace * fix(vscode): let only the owning surface decide its comment threads * fix(vscode): drop a comment removed while its delivery was still in flight * test(vscode): cover the in-flight comment removal guard * test(vscode): cover comment removal reaching every chat surface * fix(vscode): give up on a comment the chat never confirmed holding * fix(vscode): retract a comment everywhere before reporting it discarded * fix(chat): preserve queued comment cards * fix: preserve inline comment context across send paths * fix(chat): preserve command routing with context * fix(chat): keep unavailable actions on normal send path --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d323b51a0a
commit
a12b9be443
@@ -0,0 +1,67 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { useInlineCommentDraftStore } from './useInlineCommentDraftStore';
|
||||
|
||||
const comment = {
|
||||
source: 'file' as const,
|
||||
fileLabel: 'src/app.ts:12',
|
||||
startLine: 12,
|
||||
endLine: 12,
|
||||
code: 'const x = 1',
|
||||
language: 'typescript',
|
||||
text: 'fix this',
|
||||
};
|
||||
const target = { directory: '/repo', sessionKey: 'session-1' };
|
||||
|
||||
const store = () => useInlineCommentDraftStore.getState();
|
||||
|
||||
describe('caller-provided draft ids', () => {
|
||||
afterEach(() => { useInlineCommentDraftStore.setState({ drafts: {}, touchedAt: {} }); });
|
||||
|
||||
test('a caller that owns its own view of the draft chooses the id', () => {
|
||||
// The VS Code editor thread mints the id so it can track its draft without
|
||||
// waiting for a round trip.
|
||||
const id = store().addDraft(target, { ...comment, id: 'icd-editor-thread' });
|
||||
expect(id).toBe('icd-editor-thread');
|
||||
expect(store().getDrafts(target)[0].id).toBe('icd-editor-thread');
|
||||
});
|
||||
|
||||
test('the chosen id is what removal and lookup accept', () => {
|
||||
store().addDraft(target, { ...comment, id: 'icd-editor-thread' });
|
||||
store().removeDraft(target, 'icd-editor-thread');
|
||||
expect(store().getDrafts(target)).toEqual([]);
|
||||
});
|
||||
|
||||
test('omitting the id still generates one', () => {
|
||||
const id = store().addDraft(target, comment);
|
||||
expect(/^icd-\d+-\w+$/.test(id ?? '')).toBe(true);
|
||||
});
|
||||
|
||||
test('a blank id is ignored rather than stored', () => {
|
||||
const id = store().addDraft(target, { ...comment, id: ' ' });
|
||||
expect(/^icd-\d+-\w+$/.test(id ?? '')).toBe(true);
|
||||
});
|
||||
|
||||
test('a colliding id is refused, so edits cannot retarget another draft', () => {
|
||||
const first = store().addDraft(target, { ...comment, id: 'icd-duplicate' });
|
||||
const second = store().addDraft(target, { ...comment, id: 'icd-duplicate', text: 'different' });
|
||||
|
||||
expect(first).toBe('icd-duplicate');
|
||||
expect(second).not.toBe('icd-duplicate');
|
||||
|
||||
const drafts = store().getDrafts(target);
|
||||
expect(drafts).toHaveLength(2);
|
||||
expect(new Set(drafts.map((draft) => draft.id)).size).toBe(2);
|
||||
});
|
||||
|
||||
test('the same id may be reused once its draft is gone', () => {
|
||||
store().addDraft(target, { ...comment, id: 'icd-reused' });
|
||||
store().removeDraft(target, 'icd-reused');
|
||||
expect(store().addDraft(target, { ...comment, id: 'icd-reused' })).toBe('icd-reused');
|
||||
});
|
||||
|
||||
test('an id taken in another session does not collide', () => {
|
||||
const other = { directory: '/repo', sessionKey: 'session-2' };
|
||||
store().addDraft(target, { ...comment, id: 'icd-shared' });
|
||||
expect(store().addDraft(other, { ...comment, id: 'icd-shared' })).toBe('icd-shared');
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,11 @@ interface InlineCommentDraftState {
|
||||
}
|
||||
|
||||
interface InlineCommentDraftActions {
|
||||
addDraft: (target: InlineCommentDraftTarget, draft: Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>) => string | null;
|
||||
// Returns the new draft id, or null when the draft is rejected (unresolved
|
||||
// target, bounds eviction, or an empty terminal-context selection).
|
||||
// `id` lets an external owner (the VS Code editor comment thread) choose the
|
||||
// draft id up front. Omitted by every in-app caller, which gets a generated one.
|
||||
addDraft: (target: InlineCommentDraftTarget, draft: Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'> & { id?: string }) => string | null;
|
||||
updateDraft: (target: InlineCommentDraftTarget, draftId: string, updates: Partial<Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>>) => void;
|
||||
removeDraft: (target: InlineCommentDraftTarget, draftId: string) => void;
|
||||
clearDrafts: (target: InlineCommentDraftTarget) => void;
|
||||
@@ -241,7 +245,15 @@ export const useInlineCommentDraftStore = create<InlineCommentDraftStore>()(
|
||||
addDraft: (target, draft) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key || (draft.source === 'terminal' && !draft.code.trim())) return null;
|
||||
const id = `icd-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
// A caller that owns its own view of the draft (the VS Code editor
|
||||
// thread) supplies the id so it can correlate without a round trip.
|
||||
// A colliding id would silently retarget edits and removals at an
|
||||
// unrelated draft, so it is refused rather than reused.
|
||||
const requestedId = draft.id?.trim();
|
||||
const idIsTaken = Boolean(requestedId) && (get().drafts[key] ?? []).some((item) => item.id === requestedId);
|
||||
const id = requestedId && !idIsTaken
|
||||
? requestedId
|
||||
: `icd-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const nextDraft: InlineCommentDraft = { ...draft, sessionKey: target.sessionKey, id, createdAt: Date.now() };
|
||||
let accepted = false;
|
||||
set((state) => {
|
||||
|
||||
Reference in New Issue
Block a user