diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts
index fc34e42..a56dfc5 100644
--- a/src/client/src/appState.ts
+++ b/src/client/src/appState.ts
@@ -28,6 +28,7 @@ export interface AppState {
gitStatus: GitStatusResponse | undefined;
selectedDiffPath: string | undefined;
selectedDiff: GitDiffResponse | undefined;
+ selectedStagedDiff: GitDiffResponse | undefined;
gitStale: boolean;
error: string;
}
@@ -60,6 +61,7 @@ export function initialAppState(): AppState {
gitStatus: undefined,
selectedDiffPath: undefined,
selectedDiff: undefined,
+ selectedStagedDiff: undefined,
gitStale: false,
error: "",
};
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
index 32705ea..a935a29 100644
--- a/src/client/src/components/PiWebApp.ts
+++ b/src/client/src/components/PiWebApp.ts
@@ -173,7 +173,7 @@ export class PiWebApp extends LitElement {
}
private renderWorkspacePanel() {
- return html` { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}>`;
+ return html` { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}>`;
}
private getActions(): AppAction[] {
diff --git a/src/client/src/components/WorkspacePanel.ts b/src/client/src/components/WorkspacePanel.ts
index 1db0501..86250c2 100644
--- a/src/client/src/components/WorkspacePanel.ts
+++ b/src/client/src/components/WorkspacePanel.ts
@@ -16,6 +16,7 @@ export class WorkspacePanel extends LitElement {
@property({ attribute: false }) gitStatus: GitStatusResponse | undefined;
@property({ attribute: false }) selectedDiffPath: string | undefined;
@property({ attribute: false }) selectedDiff: GitDiffResponse | undefined;
+ @property({ attribute: false }) selectedStagedDiff: GitDiffResponse | undefined;
@property({ type: Boolean }) gitStale = false;
@property({ attribute: false }) onSelectTool: (tool: "files" | "git") => void = () => undefined;
@property({ attribute: false }) onRefreshFiles: () => void = () => undefined;
@@ -113,11 +114,24 @@ export class WorkspacePanel extends LitElement {
private renderDiffViewer() {
if (this.selectedDiffPath === undefined || this.selectedDiffPath === "") return html`
Select a changed file.
`;
- const diff = this.selectedDiff;
- if (diff === undefined) return html`Loading diff…
`;
+ const unstaged = this.selectedDiff;
+ const staged = this.selectedStagedDiff;
+ if (unstaged === undefined || staged === undefined) return html`Loading diff…
`;
+ const diffs = [staged, unstaged].filter((diff) => diff.diff !== "");
+ if (diffs.length === 0) return html`No staged or unstaged diff.
`;
return html`
-
-
+
+ ${diffs.map((diff) => this.renderDiffSection(diff))}
+
+ `;
+ }
+
+ private renderDiffSection(diff: GitDiffResponse) {
+ return html`
+
`;
}
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts
index 7eb1753..b8e991d 100644
--- a/src/client/src/components/shared.ts
+++ b/src/client/src/components/shared.ts
@@ -73,6 +73,10 @@ export const workspacePanelStyles = css`
.row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary { margin: 4px 6px 8px; color: #8b949e; }
.viewer { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
+ .diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
+ .diffs.single { grid-template-rows: minmax(0, 1fr); }
+ .diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid #30363d; }
+ .diff-section:last-child { border-bottom: 0; }
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; background: #0d1117; }
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
code-viewer { flex: 1 1 auto; min-height: 0; }
diff --git a/src/client/src/controllers/gitController.ts b/src/client/src/controllers/gitController.ts
index c31c113..708d51f 100644
--- a/src/client/src/controllers/gitController.ts
+++ b/src/client/src/controllers/gitController.ts
@@ -22,7 +22,7 @@ export class GitController {
if (selectedDiffPath !== undefined) {
if (status.files.some((file) => file.path === selectedDiffPath)) await this.refreshDiff(selectedDiffPath);
else {
- this.setState({ selectedDiffPath: undefined, selectedDiff: undefined });
+ this.setState({ selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined });
this.updateUrl();
}
}
@@ -32,7 +32,7 @@ export class GitController {
}
async selectDiff(path: string): Promise {
- this.setState({ selectedDiffPath: path, selectedDiff: undefined, workspaceTool: "git", mainView: this.getState().mainView === "chat" ? "chat" : "git" });
+ this.setState({ selectedDiffPath: path, selectedDiff: undefined, selectedStagedDiff: undefined, workspaceTool: "git", mainView: this.getState().mainView === "chat" ? "chat" : "git" });
this.updateUrl();
await this.refreshDiff(path);
}
@@ -42,7 +42,11 @@ export class GitController {
const workspace = this.getState().selectedWorkspace;
if (project === undefined || workspace === undefined) return;
try {
- this.setState({ selectedDiff: await api.gitDiff(project.id, workspace.id, { path }), error: "" });
+ const [selectedDiff, selectedStagedDiff] = await Promise.all([
+ api.gitDiff(project.id, workspace.id, { path }),
+ api.gitDiff(project.id, workspace.id, { path, staged: true }),
+ ]);
+ this.setState({ selectedDiff, selectedStagedDiff, error: "" });
} catch (error) {
this.setState({ error: String(error) });
}
diff --git a/src/client/src/controllers/workspaceController.ts b/src/client/src/controllers/workspaceController.ts
index 688324c..96096dd 100644
--- a/src/client/src/controllers/workspaceController.ts
+++ b/src/client/src/controllers/workspaceController.ts
@@ -12,7 +12,7 @@ export class WorkspaceController {
async selectProject(project: Project, target?: RouteTarget) {
this.sessions.clearActiveSession();
- this.setState({ selectedProject: project, selectedWorkspace: undefined, sessions: [], workspaces: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, gitStale: false, error: "" });
+ this.setState({ selectedProject: project, selectedWorkspace: undefined, sessions: [], workspaces: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined, gitStale: false, error: "" });
try {
const workspaces = await api.workspaces(project.id);
this.setState({ workspaces });
@@ -26,7 +26,7 @@ export class WorkspaceController {
async selectWorkspace(workspace: Workspace, target?: { sessionId?: string | undefined; updateUrl?: boolean | undefined }) {
this.sessions.clearActiveSession();
- this.setState({ selectedWorkspace: workspace, sessions: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, gitStale: false, error: "" });
+ this.setState({ selectedWorkspace: workspace, sessions: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined, gitStale: false, error: "" });
try {
const sessions = await api.sessions(workspace.path);
this.setState({ sessions });
diff --git a/src/server/git/gitService.ts b/src/server/git/gitService.ts
index ba864a8..62d4a51 100644
--- a/src/server/git/gitService.ts
+++ b/src/server/git/gitService.ts
@@ -13,18 +13,28 @@ export async function gitStatus(cwd: string): Promise {
export async function gitDiff(cwd: string, options: { path?: string; staged?: boolean }): Promise {
const staged = options.staged === true;
+ let path: string | undefined;
+ if (options.path !== undefined && options.path !== "") path = normalizeRelativePath(options.path);
+
const args = ["diff", "--no-ext-diff", "--color=never"];
if (staged) args.push("--cached");
- let path: string | undefined;
- if (options.path !== undefined && options.path !== "") {
- path = normalizeRelativePath(options.path);
- args.push("--", path);
- }
+ if (path !== undefined) args.push("--", path);
+
const result = await runGit(cwd, args);
if (result.code !== 0) throw new Error(result.stderr.trim() || "git diff failed");
+ if (!staged && path !== undefined && result.stdout === "" && await isUntracked(cwd, path)) {
+ const untracked = await runGit(cwd, ["diff", "--no-ext-diff", "--color=never", "--no-index", "/dev/null", "--", path]);
+ if (untracked.code !== 0 && untracked.code !== 1) throw new Error(untracked.stderr.trim() || "git diff failed");
+ return { path, staged, hash: hash(untracked.stdout), diff: untracked.stdout, truncated: untracked.truncated };
+ }
return { ...(path === undefined ? {} : { path }), staged, hash: hash(result.stdout), diff: result.stdout, truncated: result.truncated };
}
+async function isUntracked(cwd: string, path: string): Promise {
+ const result = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", path]);
+ return result.code === 0 && result.stdout.split("\0").includes(path);
+}
+
function parseStatus(raw: string): GitStatusResponse {
const records = raw.split("\0").filter((record) => record !== "");
const files: GitStatusFile[] = [];