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
@@ -9,12 +9,40 @@ import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
import { resolveWorkspaceFolders } from './workspaceResolver';
|
||||
import { pickActivePanelId } from './activePanelRouting';
|
||||
import { broadcastRemoval, drainPending } from './inlineCommentSelection';
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
type LineCommentPayload = {
|
||||
draftId?: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
source: 'diff' | 'file';
|
||||
side?: 'original' | 'modified';
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
code: string;
|
||||
language: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
type SessionPanelState = {
|
||||
/** This panel's id, which is also its surface identity for comment threads. */
|
||||
id: string;
|
||||
panel: vscode.WebviewPanel;
|
||||
sseStreams: Map<string, AbortController>;
|
||||
/**
|
||||
* Comments held until the webview proves it is listening. Posting into a
|
||||
* panel whose script has not booted drops the message outright, and the user
|
||||
* already saw the comment accepted.
|
||||
*
|
||||
* A list, because a second comment can be written while the panel is still
|
||||
* starting; a single slot silently discarded the first.
|
||||
*/
|
||||
pendingLineComments: LineCommentPayload[];
|
||||
/** Set by the panel's first inbound message, the only proof its script runs. */
|
||||
webviewReady?: boolean;
|
||||
};
|
||||
|
||||
type ActiveEditorFilePayload = {
|
||||
@@ -119,8 +147,10 @@ export class SessionEditorPanelProvider {
|
||||
};
|
||||
|
||||
const state: SessionPanelState = {
|
||||
id: panelId,
|
||||
panel,
|
||||
sseStreams: new Map(),
|
||||
pendingLineComments: [],
|
||||
};
|
||||
|
||||
this._panels.set(panelId, state);
|
||||
@@ -146,6 +176,29 @@ export class SessionEditorPanelProvider {
|
||||
}, null, this._context.subscriptions);
|
||||
|
||||
panel.webview.onDidReceiveMessage(async (message: BridgeRequest) => {
|
||||
// Any inbound message proves the webview script is running, which is the
|
||||
// only readiness signal this panel has. Flush whatever was held for it.
|
||||
state.webviewReady = true;
|
||||
for (const pending of drainPending(state.pendingLineComments)) {
|
||||
void panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'addLineComment',
|
||||
payload: pending,
|
||||
});
|
||||
}
|
||||
|
||||
// Editor comment threads mirror the composer's drafts, so the webview
|
||||
// reports every change. One-way notification, no response expected.
|
||||
if (message.type === 'inlineComments:sync') {
|
||||
// Tagged with this panel's identity: a snapshot only speaks for the
|
||||
// store that produced it, and every panel has its own.
|
||||
void vscode.commands.executeCommand('openchamber.internal.inlineCommentsSync', {
|
||||
snapshot: message.payload,
|
||||
surfaceId: panelId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'restartApi') {
|
||||
await this._openCodeManager?.restart();
|
||||
return;
|
||||
@@ -246,8 +299,10 @@ export class SessionEditorPanelProvider {
|
||||
}
|
||||
|
||||
private _getActivePanelEntry(): SessionPanelState | null {
|
||||
const activeEntry = Array.from(this._panels.entries()).find(([, entry]) => entry.panel.active);
|
||||
const panelId = activeEntry?.[0] ?? this._lastActivePanelId;
|
||||
const panelId = pickActivePanelId(
|
||||
Array.from(this._panels.entries()).map(([id, entry]) => ({ id, active: entry.panel.active })),
|
||||
this._lastActivePanelId,
|
||||
);
|
||||
if (!panelId) {
|
||||
return null;
|
||||
}
|
||||
@@ -274,6 +329,105 @@ export class SessionEditorPanelProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
public addLineCommentToActivePanel(payload: {
|
||||
draftId?: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
source: 'diff' | 'file';
|
||||
side?: 'original' | 'modified';
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
code: string;
|
||||
language: string;
|
||||
comment: string;
|
||||
}): string | null {
|
||||
if (!payload.relativePath.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = this._getActivePanelEntry();
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
entry.panel.reveal(entry.panel.viewColumn ?? vscode.ViewColumn.Active, true);
|
||||
|
||||
// An existing panel can still be booting (reopened from a restored window),
|
||||
// and a post into a webview whose script has not run is dropped outright.
|
||||
// Hold it on the same path a freshly opened panel uses.
|
||||
if (!entry.webviewReady) {
|
||||
entry.pendingLineComments.push(payload);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
void entry.panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'addLineComment',
|
||||
payload,
|
||||
});
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers a comment to a session tab, opening one when none exists.
|
||||
*
|
||||
* A comment is written against code the user is reading, so it must not
|
||||
* depend on their having opened a chat first. With no tab open this behaves
|
||||
* like the toolbar's new-session button, then delivers into that tab once its
|
||||
* webview is listening.
|
||||
*/
|
||||
public openWithLineComment(payload: LineCommentPayload, activeSessionId: string | null): string | null {
|
||||
if (!payload.relativePath.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accepted = this.addLineCommentToActivePanel(payload);
|
||||
if (accepted) {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
if (activeSessionId) {
|
||||
this.createOrShow(activeSessionId);
|
||||
} else {
|
||||
this.createOrShowNewSession();
|
||||
}
|
||||
|
||||
const entry = this._getActivePanelEntry();
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
entry.pendingLineComments.push(payload);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a draft the user removed from its editor thread.
|
||||
*
|
||||
* Sent to every panel, not just the active one: each webview owns its own
|
||||
* draft store, and the draft may have landed in a tab the user has since
|
||||
* moved away from. Targeting only the active panel made removal a silent
|
||||
* no-op in that case, leaving the chip attached after its thread was gone.
|
||||
*
|
||||
* Unlike adding, this does not reveal a panel: the user is looking at the
|
||||
* code, and stealing focus to show a chip disappearing would be worse than
|
||||
* letting it disappear quietly.
|
||||
*/
|
||||
public removeLineComment(draftId: string): void {
|
||||
const targets = [...this._panels.values()].map((state) => ({
|
||||
pendingLineComments: state.pendingLineComments,
|
||||
notify: () => {
|
||||
void state.panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'removeLineComment',
|
||||
payload: { draftId },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
broadcastRemoval(targets, draftId);
|
||||
}
|
||||
|
||||
public createSessionWithPromptInActivePanel(prompt: string): boolean {
|
||||
if (!prompt.trim()) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user