Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/chat-view-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "node:path";
import * as vscode from "vscode";
import type {
Event,
FileDiff,
Message,
OpenCodeConnection,
OpenCodePath,
Expand Down Expand Up @@ -36,7 +37,8 @@ export type ExtToWebviewMessage =
| { type: "contextUsage"; usage: { inputTokens: number; contextLimit: number } }
| { type: "toolConfig"; paths: OpenCodePath }
| { type: "locale"; vscodeLanguage: string }
| { type: "modelUpdated"; model: string; default: Record<string, string> };
| { type: "modelUpdated"; model: string; default: Record<string, string> }
| { type: "sessionDiff"; sessionId: string; diffs: FileDiff[] };

// --- Webview → Extension Host ---
export type WebviewToExtMessage =
Expand Down Expand Up @@ -71,6 +73,8 @@ export type WebviewToExtMessage =
| { type: "openConfigFile"; filePath: string }
| { type: "openTerminal" }
| { type: "setModel"; model: string }
| { type: "getSessionDiff"; sessionId: string }
| { type: "openDiffEditor"; filePath: string; before: string; after: string }
| { type: "ready" };

export class ChatViewProvider implements vscode.WebviewViewProvider {
Expand Down Expand Up @@ -326,6 +330,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
this.postMessage({ type: "modelUpdated", model: message.model, default: {} });
break;
}
case "getSessionDiff": {
const diffs = await this.connection.getSessionDiff(message.sessionId);
this.postMessage({ type: "sessionDiff", sessionId: message.sessionId, diffs });
break;
}
case "openDiffEditor": {
// 仮想ドキュメントを使って VS Code のネイティブ diff エディタを開く
const beforeUri = vscode.Uri.parse(
`opencode-diff-before:${message.filePath}?${encodeURIComponent(message.before)}`,
);
const afterUri = vscode.Uri.parse(
`opencode-diff-after:${message.filePath}?${encodeURIComponent(message.after)}`,
);
const fileName = path.basename(message.filePath);
await vscode.commands.executeCommand("vscode.diff", beforeUri, afterUri, `${fileName} (Changes)`);
break;
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export async function activate(context: vscode.ExtensionContext) {
const chatViewProvider = new ChatViewProvider(context.extensionUri, connection);
context.subscriptions.push(vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chatViewProvider));

// diff エディタ用の仮想ドキュメントプロバイダー。
// URI のクエリ部分にエンコードされたコンテンツを返す。
const diffContentProvider: vscode.TextDocumentContentProvider = {
provideTextDocumentContent(uri: vscode.Uri): string {
return decodeURIComponent(uri.query);
},
};
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider("opencode-diff-before", diffContentProvider),
vscode.workspace.registerTextDocumentContentProvider("opencode-diff-after", diffContentProvider),
);

context.subscriptions.push(new vscode.Disposable(() => connection.disconnect()));
}

Expand Down
13 changes: 12 additions & 1 deletion src/opencode-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createOpencodeClient,
createOpencodeServer,
type Event,
type FileDiff,
type McpStatus,
type Message,
type Path as OpenCodePath,
Expand All @@ -15,7 +16,7 @@ import {
} from "@opencode-ai/sdk";
import * as vscode from "vscode";

export type { Event, Session, Message, Part, Provider, McpStatus, ToolListItem, Config, OpenCodePath };
export type { Event, Session, Message, Part, Provider, McpStatus, ToolListItem, Config, OpenCodePath, FileDiff };

// provider.list() が返す生データ型
export type ProviderListResult = {
Expand Down Expand Up @@ -245,6 +246,16 @@ export class OpenCodeConnection {
});
}

// --- Session Diff API ---

async getSessionDiff(sessionId: string): Promise<FileDiff[]> {
const client = this.requireClient();
const response = await client.session.diff({
path: { id: sessionId },
});
return response.data!;
}

// --- Revert API ---

async revertSession(sessionId: string, messageID: string): Promise<Session> {
Expand Down
36 changes: 35 additions & 1 deletion webview/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Event } from "@opencode-ai/sdk";
import { useCallback, useEffect, useState } from "react";
import { EmptyState } from "./components/molecules/EmptyState";
import { FileChangesHeader } from "./components/molecules/FileChangesHeader";
import { TodoHeader } from "./components/molecules/TodoHeader";
import { ChatHeader } from "./components/organisms/ChatHeader";
import { InputArea } from "./components/organisms/InputArea";
import { MessagesArea } from "./components/organisms/MessagesArea";
import { SessionList } from "./components/organisms/SessionList";
import { AppContextProvider, type AppContextValue } from "./contexts/AppContext";
import { useFileChanges } from "./hooks/useFileChanges";
import { useLocale } from "./hooks/useLocale";
import { useMessages } from "./hooks/useMessages";
import { usePermissions } from "./hooks/usePermissions";
Expand All @@ -25,6 +27,7 @@ export function App() {
const prov = useProviders();
const perm = usePermissions();
const locale = useLocale();
const fileChanges = useFileChanges();

// Extension Host → Webview メッセージでのみ更新される単純なステート
const [openEditors, setOpenEditors] = useState<FileAttachment[]>([]);
Expand All @@ -50,8 +53,20 @@ export function App() {
session.handleSessionEvent(event);
msg.handleMessageEvent(event);
perm.handlePermissionEvent(event);
fileChanges.handleFileChangeEvent(event);

// file.edited イベント時にセッション差分を再取得する
if (event.type === "file.edited" && session.activeSession) {
postMessage({ type: "getSessionDiff", sessionId: session.activeSession.id });
}
},
[session.handleSessionEvent, msg.handleMessageEvent, perm.handlePermissionEvent],
[
session.handleSessionEvent,
session.activeSession,
msg.handleMessageEvent,
perm.handlePermissionEvent,
fileChanges.handleFileChangeEvent,
],
);

// Extension Host → Webview message listener
Expand All @@ -71,8 +86,10 @@ export function App() {
session.setActiveSession(data.session);
if (data.session) {
postMessage({ type: "getMessages", sessionId: data.session.id });
postMessage({ type: "getSessionDiff", sessionId: data.session.id });
} else {
msg.setMessages([]);
fileChanges.clearDiffs();
}
break;
case "event":
Expand Down Expand Up @@ -117,6 +134,12 @@ export function App() {
}
break;
}
case "sessionDiff": {
if (data.sessionId === session.activeSession?.id) {
fileChanges.setDiffs(data.diffs);
}
break;
}
}
};
window.addEventListener("message", handler);
Expand All @@ -128,6 +151,8 @@ export function App() {
handleEvent,
locale.setVscodeLanguage,
msg.setMessages,
fileChanges.setDiffs,
fileChanges.clearDiffs,
prov.setAllProvidersData,
prov.setProviders,
prov.setSelectedModel,
Expand Down Expand Up @@ -229,6 +254,10 @@ export function App() {
[session.activeSession, msg],
);

const handleOpenDiffEditor = useCallback((filePath: string, before: string, after: string) => {
postMessage({ type: "openDiffEditor", filePath, before, after });
}, []);

const contextValue: AppContextValue = {
sessions: session.sessions,
activeSession: session.activeSession,
Expand All @@ -251,6 +280,8 @@ export function App() {
permissions: perm.permissions,
openEditors,
workspaceFiles,
fileDiffs: fileChanges.diffs,
onOpenDiffEditor: handleOpenDiffEditor,
onSend: handleSend,
onShellExecute: handleShellExecute,
isShellMessage: msg.isShellMessage,
Expand Down Expand Up @@ -295,6 +326,9 @@ export function App() {
onRevertToCheckpoint={handleRevertToCheckpoint}
/>
{msg.latestTodos.length > 0 && <TodoHeader todos={msg.latestTodos} />}
{fileChanges.diffs.length > 0 && (
<FileChangesHeader diffs={fileChanges.diffs} onOpenDiffEditor={handleOpenDiffEditor} />
)}
<InputArea
onSend={handleSend}
onShellExecute={handleShellExecute}
Expand Down
10 changes: 10 additions & 0 deletions webview/__tests__/components/atoms/icons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
ClipIcon,
CloseIcon,
DeleteIcon,
DiffIcon,
EditActionIcon,
EditIcon,
ErrorCircleIcon,
ExternalLinkIcon,
EyeIcon,
EyeOffIcon,
FileIcon,
Expand Down Expand Up @@ -210,3 +212,11 @@ describe("EyeIcon", () => {
describe("EyeOffIcon", () => {
describeIconComponent(EyeOffIcon, 14, 14);
});

describe("DiffIcon", () => {
describeIconComponent(DiffIcon, 16, 16);
});

describe("ExternalLinkIcon", () => {
describeIconComponent(ExternalLinkIcon, 12, 12);
});
Loading