Stage, unstage, and discard individual diff hunks

Add per-hunk staging, unstaging, and discarding to the Changes diff
view, so a single change region inside a file can be acted on in
isolation instead of forcing whole-file stage/revert. The change is
wired end-to-end across the web server, the shared UI runtime API
contract, and the VS Code extension, with Electron inheriting the web
path unchanged (it boots the server in-process).

Server
------
- New `applyHunk(directory, filePath, { patch, action })` in
  packages/web/server/lib/git/service.js. It resolves the repository
  context and validates the file path with the same helpers used by
  stageFiles/unstageFiles (resolveGitFileContext +
  validateRepositoryFilePaths), then writes the single-hunk patch to a
  temporary file in the OS temp dir (never inside the repo, so it
  cannot show up as an untracked file) and runs `git apply` with flags
  chosen per action:
    stage   -> git apply --cached          (working tree -> index)
    unstage -> git apply --cached --reverse (index -> working tree)
    discard -> git apply --reverse          (revert in working tree)
  A `git apply --check` runs first with the same flags, so a stale
  hunk that no longer applies fails with a clear "Hunk no longer
  applies - refresh and try again" message instead of leaving a
  partial mutation. The patch's target path is parsed and must match
  the requested file (with /dev/null tolerated for new/deleted files),
  preventing a patch from silently targeting a different path. The
  whole operation runs inside withGitIndexMutationQueue to avoid
  racing with concurrent stage/unstage. The temp file is removed in a
  finally block.
- New `POST /api/git/apply-hunk` route in routes.js, registered
  alongside stage/unstage. Validates directory, path, non-empty patch,
  and action before delegating.
- DOCUMENTATION.md updated with the new service entry.

Patch extraction
----------------
- packages/ui/src/lib/diff/patchFileDiff.ts gains
  splitPatchIntoHunks(patch) and extractHunkPatch(patch, hunkIndex).
  They keep the original file header (diff --git / index / --- / +++)
  and emit exactly one @@ hunk per standalone patch, which is what
  `git apply` expects. Each emitted patch is guaranteed to end with a
  trailing newline (without it git apply reports "corrupt patch").

Runtime API contract
--------------------
- GitAPI (packages/ui/src/lib/api/types.ts) gains optional
  stageGitHunk / unstageGitHunk / revertGitHunk, matching the
  stageGitFiles? / unstageGitFiles? precedent so runtimes that do not
  support it degrade gracefully.
- gitApi.ts delegates to the registered runtime git API, falling back
  to gitApiHttp, exactly like the existing whole-file helpers.
- gitApiHttp.ts posts to /api/git/apply-hunk.
- Web runtime composes the three methods in packages/web/src/api/git.ts.

VS Code parity
--------------
- packages/vscode/src/gitService.ts adds applyGitHunk(), implemented
  natively with the existing execGit helper + a temp patch file +
  `git apply` (--cached / --cached --reverse / --reverse), mirroring
  the server's --check-first safety and temp-file cleanup.
- bridge-git-runtime.ts handles the new api:git/apply-hunk bridge
  message; webview/api/git.ts sends it. VS Code users get identical
  stage/unstage/discard-hunk behavior.

UI
--
- New DiffHunkActions component renders a compact per-hunk strip
  above each expanded file diff in the Changes view. Each hunk chip
  shows its +additions / -deletions counts and offers:
    working scope -> Stage + Discard
    staged scope  -> Unstage
  Clicking extracts that hunk's standalone patch via
  extractHunkPatch(patch, hunkIndex) and calls the runtime git API.
  Because the chip index comes directly from fileDiff.hunks[] and the
  patch is sliced in the same order, the hunk the user sees is always
  the hunk that gets applied. While any action is in flight all buttons
  disable to prevent conflicting concurrent mutations; the per-hunk
  spinner reflects in-flight state.
- DiffView wires DiffHunkActions into InlineDiffViewer (text diffs
  only; binary/image and full-file-content modes are excluded since
  they have no patch). MultiFileDiffEntry passes directory/staged
  through and handles onHunkApplied by bumping the diff reload nonce
  (so the file's diff re-fetches and the affected hunk disappears)
  and refreshing git status (so file counts and the staged/changed
  scope update). Hunk actions are therefore available wherever the
  default patch-context diff is shown.

i18n
----
- 10 new keys (diffView.hunk.*) added to all 9 locales (en, es, fr,
  ko, pl, pt-BR, uk, zh-CN, zh-TW), including stage/unstage/discard
  labels, tooltips with the hunk index, a stale-hunk error message,
  and an unsupported-runtime fallback.

Tests
-----
- packages/ui/src/lib/diff/patchFileDiff.test.ts covers
  splitHunks/extractHunkPatch: multi-hunk split, header preservation,
  single-hunk and empty patches, out-of-range indices.
- service.test.js adds an applyHunk suite that builds real temp repos
  with two separate hunks and verifies: staging one hunk leaves the
  other unstaged, discarding reverts only the targeted hunk in the
  working tree, unstaging removes only one hunk from the index, and a
  retargeted patch (different file path) is rejected. Also covers
  invalid-action / missing-hunk-header validation.
- packages/web/src/api/git.test.ts mock completed with the new methods
  (and previously-missing exports that prevented the test from
  loading) and asserts the three hunk methods are exposed.
- routes.test.js continues to pass under bun.

CHANGELOG updated under [Unreleased].
This commit is contained in:
Bohdan Triapitsyn
2026-06-14 10:58:23 +03:00
parent 1c281df3b7
commit f645d57c93
26 changed files with 811 additions and 34 deletions
@@ -0,0 +1,158 @@
import React from 'react';
import type { FileDiffMetadata } from '@pierre/diffs';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { extractHunkPatch } from '@/lib/diff/patchFileDiff';
type HunkAction = 'stage' | 'unstage' | 'discard';
interface DiffHunkActionsProps {
patch: string;
fileDiff: FileDiffMetadata | undefined;
directory: string;
filePath: string;
staged: boolean;
onApplied: (action: HunkAction) => void;
}
export const DiffHunkActions = React.memo<DiffHunkActionsProps>(({
patch,
fileDiff,
directory,
filePath,
staged,
onApplied,
}) => {
const { t } = useI18n();
const { git } = useRuntimeAPIs();
const [busyKey, setBusyKey] = React.useState<string | null>(null);
const [error, setError] = React.useState<string | null>(null);
const hunks = fileDiff?.hunks;
if (!hunks || hunks.length === 0 || !patch) {
return null;
}
const run = async (hunkIndex: number, action: HunkAction) => {
const hunkPatch = extractHunkPatch(patch, hunkIndex);
if (!hunkPatch) {
setError(t('diffView.hunk.unavailable'));
return;
}
const key = `${hunkIndex}:${action}`;
setBusyKey(key);
setError(null);
try {
if (action === 'stage') {
if (!git.stageGitHunk) throw new Error(t('diffView.hunk.unsupported'));
await git.stageGitHunk(directory, filePath, hunkPatch);
} else if (action === 'unstage') {
if (!git.unstageGitHunk) throw new Error(t('diffView.hunk.unsupported'));
await git.unstageGitHunk(directory, filePath, hunkPatch);
} else {
if (!git.revertGitHunk) throw new Error(t('diffView.hunk.unsupported'));
await git.revertGitHunk(directory, filePath, hunkPatch);
}
onApplied(action);
} catch (actionError) {
setError(actionError instanceof Error ? actionError.message : String(actionError));
} finally {
setBusyKey((current) => (current === key ? null : current));
}
};
return (
<div className="flex flex-col gap-1 border-b border-[var(--interactive-border)]/40 bg-[var(--surface-elevated)]/40 px-3 py-1.5">
<div className="flex items-center gap-1.5 overflow-x-auto">
<span className="typography-micro shrink-0 text-muted-foreground uppercase">
{t('diffView.hunk.label')}
</span>
{hunks.map((hunk, index) => {
const additions = hunk.additionLines;
const deletions = hunk.deletionLines;
return (
<div
key={index}
className="flex shrink-0 items-center gap-1 rounded-md border border-[var(--interactive-border)]/50 bg-background/60 px-1.5 py-0.5"
>
<span className="typography-micro font-semibold text-muted-foreground">
{String(index + 1).padStart(2, '0')}
</span>
{additions > 0 ? (
<span className="typography-micro" style={{ color: 'var(--status-success)' }}>
+{additions}
</span>
) : null}
{deletions > 0 ? (
<span className="typography-micro" style={{ color: 'var(--status-error)' }}>
{deletions}
</span>
) : null}
{staged ? (
<Button
variant="ghost"
size="xs"
className="h-5 gap-1 px-1.5"
disabled={busyKey !== null}
onClick={() => void run(index, 'unstage')}
title={t('diffView.hunk.unstageTitle', { index: index + 1 })}
>
{busyKey === `${index}:unstage` ? (
<Icon name="loader-4" className="size-3 animate-spin" />
) : (
<Icon name="arrow-go-back" className="size-3" />
)}
{t('diffView.hunk.unstage')}
</Button>
) : (
<>
<Button
variant="ghost"
size="xs"
className="h-5 gap-1 px-1.5"
disabled={busyKey !== null}
onClick={() => void run(index, 'stage')}
title={t('diffView.hunk.stageTitle', { index: index + 1 })}
>
{busyKey === `${index}:stage` ? (
<Icon name="loader-4" className="size-3 animate-spin" />
) : (
<Icon name="add" className="size-3" />
)}
{t('diffView.hunk.stage')}
</Button>
<Button
variant="ghost"
size="xs"
className="h-5 gap-1 px-1.5 text-muted-foreground hover:text-[var(--status-error)]"
disabled={busyKey !== null}
onClick={() => void run(index, 'discard')}
title={t('diffView.hunk.discardTitle', { index: index + 1 })}
>
{busyKey === `${index}:discard` ? (
<Icon name="loader-4" className="size-3 animate-spin" />
) : (
<Icon name="close" className="size-3" />
)}
{t('diffView.hunk.discard')}
</Button>
</>
)}
</div>
);
})}
</div>
{error ? (
<div className="flex items-center gap-1.5 typography-meta" style={{ color: 'var(--status-error)' }}>
<Icon name="error-warning" className="size-3.5 shrink-0" />
<span className={cn('min-w-0')}>{error}</span>
</div>
) : null}
</div>
);
});
+62 -34
View File
@@ -26,6 +26,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { DiffViewToggle } from '@/components/chat/message/DiffViewToggle';
import type { DiffViewMode } from '@/components/chat/message/types';
import { PierreDiffViewer } from './PierreDiffViewer';
import { DiffHunkActions } from './DiffHunkActions';
import { useDeviceInfo } from '@/lib/device';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { Icon } from "@/components/icon/Icon";
@@ -481,51 +482,67 @@ const InlineImageDiffViewer = React.memo<InlineImageDiffViewerProps>(({
});
interface InlineDiffViewerProps {
filePath: string;
diff: DiffData;
renderSideBySide: boolean;
wrapLines: boolean;
filePath: string;
diff: DiffData;
renderSideBySide: boolean;
wrapLines: boolean;
directory: string;
staged: boolean;
onHunkApplied: (action: 'stage' | 'unstage' | 'discard') => void;
}
const InlineDiffViewer = React.memo<InlineDiffViewerProps>(({
filePath,
diff,
renderSideBySide,
wrapLines,
const InlineDiffViewer = React.memo<InlineDiffViewerProps>(({
filePath,
diff,
renderSideBySide,
wrapLines,
directory,
staged,
onHunkApplied,
}) => {
const language = React.useMemo(
() => getLanguageFromExtension(filePath) || 'text',
[filePath]
);
const language = React.useMemo(
() => getLanguageFromExtension(filePath) || 'text',
[filePath]
);
if (diff.isBinary) {
return <BinaryDiffPlaceholder />;
}
if (diff.isBinary) {
return <BinaryDiffPlaceholder />;
}
if (isImageFile(filePath)) {
return (
if (isImageFile(filePath)) {
return (
<InlineImageDiffViewer
filePath={filePath}
diff={diff}
renderSideBySide={renderSideBySide}
/>
);
}
return (
<div className="w-full" style={{ contain: 'layout' }}>
<PierreDiffViewer
original={diff.original}
modified={diff.modified}
fileDiff={diff.fileDiff}
language={language}
fileName={filePath}
renderSideBySide={renderSideBySide}
wrapLines={wrapLines}
layout="inline"
/>
</div>
);
}
return (
<div className="w-full" style={{ contain: 'layout' }}>
{diff.patch && diff.fileDiff ? (
<DiffHunkActions
patch={diff.patch}
fileDiff={diff.fileDiff}
directory={directory}
filePath={filePath}
staged={staged}
onApplied={onHunkApplied}
/>
) : null}
<PierreDiffViewer
original={diff.original}
modified={diff.modified}
fileDiff={diff.fileDiff}
language={language}
fileName={filePath}
renderSideBySide={renderSideBySide}
wrapLines={wrapLines}
layout="inline"
/>
</div>
);
});
interface MultiFileDiffEntryProps {
@@ -573,6 +590,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
}, [directory, file.path])
);
const setDiff = useGitStore((state) => state.setDiff);
const fetchStatus = useGitStore((state) => state.fetchStatus);
const setDiffFileLayout = useUIStore((state) => state.setDiffFileLayout);
const [diffRetryNonce, setDiffRetryNonce] = React.useState(0);
@@ -693,6 +711,13 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
handleSelect();
}, [handleOpenChange, handleSelect, isExpanded]);
const handleHunkApplied = React.useCallback(() => {
setDiffRetryNonce((nonce) => nonce + 1);
if (directory) {
void fetchStatus(directory, git);
}
}, [directory, fetchStatus, git]);
return (
<div ref={setSectionRef} className="scroll-mt-9 border-b border-[var(--interactive-border)]/40 last:border-b-0">
<div className="sticky top-0 z-10 border-b border-[var(--interactive-border)]/35 bg-[var(--surface-elevated)]/90 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--surface-elevated)]/80">
@@ -847,6 +872,9 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
diff={diffData}
renderSideBySide={renderSideBySide}
wrapLines={wrapLines}
directory={directory}
staged={staged}
onHunkApplied={handleHunkApplied}
/>
) : null}
</div>
+3
View File
@@ -473,6 +473,9 @@ export interface GitAPI {
stageGitFiles?(directory: string, filePaths: string[]): Promise<void>;
unstageGitFile(directory: string, filePath: string): Promise<void>;
unstageGitFiles?(directory: string, filePaths: string[]): Promise<void>;
stageGitHunk?(directory: string, filePath: string, patch: string): Promise<void>;
unstageGitHunk?(directory: string, filePath: string, patch: string): Promise<void>;
revertGitHunk?(directory: string, filePath: string, patch: string): Promise<void>;
isLinkedWorktree(directory: string): Promise<boolean>;
getGitBranches(directory: string): Promise<GitBranch>;
deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }>;
@@ -0,0 +1,83 @@
import { describe, expect, test } from "bun:test";
import { extractHunkPatch, splitPatchIntoHunks } from "./patchFileDiff";
const SAMPLE_PATCH = `diff --git a/foo.txt b/foo.txt
index 1111111..2222222 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1,4 +1,5 @@
line1
+added-top
line2
line3
line4
@@ -10,3 +11,4 @@
line10
-deleted-mid
line11
+added-bottom
`;
describe("splitPatchIntoHunks", () => {
test("splits a multi-hunk patch into standalone per-hunk patches", () => {
const hunks = splitPatchIntoHunks(SAMPLE_PATCH);
expect(hunks.length).toBe(2);
expect(hunks[0]).toContain("diff --git a/foo.txt b/foo.txt");
expect(hunks[0]).toContain("--- a/foo.txt");
expect(hunks[0]).toContain("+++ b/foo.txt");
expect(hunks[0]).toContain("@@ -1,4 +1,5 @@");
expect(hunks[0]).toContain("+added-top");
expect(hunks[0]).not.toContain("@@ -10,3 +11,4 @@");
expect(hunks[0]).not.toContain("added-bottom");
expect(hunks[1]).toContain("@@ -10,3 +11,4 @@");
expect(hunks[1]).toContain("-deleted-mid");
expect(hunks[1]).toContain("+added-bottom");
expect(hunks[1]).not.toContain("added-top");
});
test("each hunk keeps the file header so it applies on its own", () => {
const hunks = splitPatchIntoHunks(SAMPLE_PATCH);
for (const hunk of hunks) {
expect(hunk.startsWith("diff --git a/foo.txt b/foo.txt\n")).toBe(true);
expect(hunk.match(/^--- a\/foo.txt$/m)).not.toBeNull();
expect(hunk.match(/^\+\+\+ b\/foo.txt$/m)).not.toBeNull();
expect(hunk.match(/^@@\s/m)).not.toBeNull();
}
});
test("returns [] for an empty patch or a patch without hunks", () => {
expect(splitPatchIntoHunks("")).toEqual([]);
expect(splitPatchIntoHunks("diff --git a/foo b/foo\n--- a/foo\n+++ b/foo\n")).toEqual([]);
});
test("handles a single-hunk patch", () => {
const single = `diff --git a/a b/a
--- a/a
+++ b/a
@@ -1,1 +1,2 @@
a
+b
`;
const hunks = splitPatchIntoHunks(single);
expect(hunks.length).toBe(1);
expect(hunks[0]).toContain("+b");
});
});
describe("extractHunkPatch", () => {
test("returns the standalone patch for the requested index", () => {
const second = extractHunkPatch(SAMPLE_PATCH, 1);
expect(second).not.toBeNull();
expect(second).toContain("@@ -10,3 +11,4 @@");
expect(second).toContain("diff --git a/foo.txt b/foo.txt");
});
test("returns null for out-of-range or invalid indices", () => {
expect(extractHunkPatch(SAMPLE_PATCH, -1)).toBeNull();
expect(extractHunkPatch(SAMPLE_PATCH, 2)).toBeNull();
expect(extractHunkPatch(SAMPLE_PATCH, 1.5)).toBeNull();
expect(extractHunkPatch("", 0)).toBeNull();
});
});
+51
View File
@@ -142,3 +142,54 @@ const joinPatchLines = (lines: Array<{ text: string; newline: boolean }>): strin
const emptyFileDiff = (file: string): FileDiffMetadata =>
parseDiffFromFile({ name: file, contents: '' }, { name: file, contents: '' });
/**
* Split a unified diff patch for a single file into standalone per-hunk patches.
*
* Each returned patch preserves the original file header (everything before the
* first `@@` hunk header) plus exactly one hunk, producing a patch that can be
* fed to `git apply` on its own.
*
* Returns an empty array when no hunk headers are present.
*/
export const splitPatchIntoHunks = (patch: string): string[] => {
if (!patch) return [];
const lines = patch.split(/\r?\n/);
const hunkHeaderRegex = /^@@\s/;
const headerLines: string[] = [];
let firstHunk = 0;
while (firstHunk < lines.length && !hunkHeaderRegex.test(lines[firstHunk] ?? '')) {
headerLines.push(lines[firstHunk]);
firstHunk += 1;
}
if (firstHunk >= lines.length) {
return [];
}
const hunks: string[][] = [];
for (let index = firstHunk; index < lines.length; index += 1) {
const line = lines[index];
if (hunkHeaderRegex.test(line ?? '')) {
hunks.push([...headerLines, line]);
} else if (hunks.length > 0) {
hunks[hunks.length - 1].push(line ?? '');
}
}
return hunks.map((hunkLines) => hunkLines.join('\n'))
.filter((hunk) => hunk.trim().length > 0)
.map((hunk) => (hunk.endsWith('\n') ? hunk : `${hunk}\n`));
};
/**
* Extract a standalone patch for a single hunk by zero-based index.
*
* Returns `null` when the index is out of range or the patch has no hunks.
*/
export const extractHunkPatch = (patch: string, hunkIndex: number): string | null => {
if (!Number.isInteger(hunkIndex) || hunkIndex < 0) return null;
const hunks = splitPatchIntoHunks(patch);
return hunks[hunkIndex] ?? null;
};
+18
View File
@@ -168,6 +168,24 @@ export async function unstageGitFiles(directory: string, filePaths: string[]): P
return gitHttp.unstageGitFiles(directory, filePaths);
}
export async function stageGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
const runtime = getRuntimeGit();
if (runtime?.stageGitHunk) return runtime.stageGitHunk(directory, filePath, patch);
return gitHttp.stageGitHunk(directory, filePath, patch);
}
export async function unstageGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
const runtime = getRuntimeGit();
if (runtime?.unstageGitHunk) return runtime.unstageGitHunk(directory, filePath, patch);
return gitHttp.unstageGitHunk(directory, filePath, patch);
}
export async function revertGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
const runtime = getRuntimeGit();
if (runtime?.revertGitHunk) return runtime.revertGitHunk(directory, filePath, patch);
return gitHttp.revertGitHunk(directory, filePath, patch);
}
export async function isLinkedWorktree(directory: string): Promise<boolean> {
const runtime = getRuntimeGit();
if (runtime) return runtime.isLinkedWorktree(directory);
+37
View File
@@ -289,6 +289,43 @@ export async function unstageGitFiles(directory: string, filePaths: string[]): P
}
}
export async function stageGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
await applyGitHunk(directory, filePath, patch, 'stage');
}
export async function unstageGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
await applyGitHunk(directory, filePath, patch, 'unstage');
}
export async function revertGitHunk(directory: string, filePath: string, patch: string): Promise<void> {
await applyGitHunk(directory, filePath, patch, 'discard');
}
async function applyGitHunk(
directory: string,
filePath: string,
patch: string,
action: 'stage' | 'unstage' | 'discard',
): Promise<void> {
if (!filePath) {
throw new Error('path is required to apply a git hunk');
}
if (typeof patch !== 'string' || !patch.trim()) {
throw new Error('patch is required to apply a git hunk');
}
const response = await runtimeFetch(buildUrl(`${API_BASE}/apply-hunk`, directory), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: filePath, patch, action }),
});
if (!response.ok) {
const message = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(message.error || 'Failed to apply git hunk');
}
}
export async function isLinkedWorktree(directory: string): Promise<boolean> {
if (!directory) {
return false;
+9
View File
@@ -1215,6 +1215,15 @@ export const dict = {
'diffView.actions.enableLineWrap': 'Enable line wrap',
'diffView.actions.openFileInEditorAtChange': 'Open this file in editor at change',
'diffView.actions.openFileAtFirstChangedLine': 'Open this file at first changed line',
'diffView.hunk.label': 'Hunks',
'diffView.hunk.stage': 'Stage',
'diffView.hunk.unstage': 'Unstage',
'diffView.hunk.discard': 'Discard',
'diffView.hunk.stageTitle': 'Stage hunk {index}',
'diffView.hunk.unstageTitle': 'Unstage hunk {index}',
'diffView.hunk.discardTitle': 'Discard hunk {index}',
'diffView.hunk.unavailable': 'This hunk is no longer available. Refresh the diff and try again.',
'diffView.hunk.unsupported': 'Staging individual hunks is not supported in this runtime.',
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Select a project to add notes and todos.',
'rightSidebar.contextNotesTodo.notes.title': 'Quick notes - {project}',
+9
View File
@@ -1181,6 +1181,15 @@ export const dict: Record<I18nKey, string> = {
"diffView.actions.enableLineWrap": "Activar ajuste de línea",
"diffView.actions.openFileInEditorAtChange": "Abrir este archivo en el editor en el cambio",
"diffView.actions.openFileAtFirstChangedLine": "Abrir este archivo en la primera línea modificada",
"diffView.hunk.label": "Fragmentos",
"diffView.hunk.stage": "Preparar",
"diffView.hunk.unstage": "Quitar",
"diffView.hunk.discard": "Descartar",
"diffView.hunk.stageTitle": "Preparar fragmento {index}",
"diffView.hunk.unstageTitle": "Quitar fragmento {index} de la preparación",
"diffView.hunk.discardTitle": "Descartar fragmento {index}",
"diffView.hunk.unavailable": "Este fragmento ya no está disponible. Actualice el diff e inténtelo de nuevo.",
"diffView.hunk.unsupported": "Preparar fragmentos individuales no es compatible en este entorno.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "Plan",
"rightSidebar.contextNotesTodo.empty.selectProject": "Selecciona un proyecto para añadir notas y tareas pendientes.",
"rightSidebar.contextNotesTodo.notes.title": "Notas rápidas — {project}",
+9
View File
@@ -1088,6 +1088,15 @@ export const dict = {
'diffView.actions.enableLineWrap': 'Activer le retour à la ligne',
'diffView.actions.openFileInEditorAtChange': 'Ouvrez ce fichier dans l\'éditeur lors du changement',
'diffView.actions.openFileAtFirstChangedLine': 'Ouvrez ce fichier à la première ligne modifiée',
'diffView.hunk.label': 'Sections',
'diffView.hunk.stage': 'Préparer',
'diffView.hunk.unstage': 'Retirer',
'diffView.hunk.discard': 'Annuler',
'diffView.hunk.stageTitle': 'Préparer la section {index}',
'diffView.hunk.unstageTitle': 'Retirer la section {index} de la préparation',
'diffView.hunk.discardTitle': 'Annuler la section {index}',
'diffView.hunk.unavailable': "Cette section n'est plus disponible. Actualisez le diff et réessayez.",
'diffView.hunk.unsupported': "La préparation de sections individuelles n'est pas prise en charge dans cet environnement.",
'rightSidebar.contextNotesTodo.plan.defaultTitle': 'Plan',
'rightSidebar.contextNotesTodo.empty.selectProject': 'Sélectionnez un projet pour ajouter des notes et des tâches.',
'rightSidebar.contextNotesTodo.notes.title': 'Notes rapides - {project}',
+9
View File
@@ -1218,6 +1218,15 @@ export const dict: Record<I18nKey, string> = {
'diffView.actions.enableLineWrap': '줄 바꿈 켜기',
'diffView.actions.openFileInEditorAtChange': '변경 위치에서 이 파일을 에디터로 열기',
'diffView.actions.openFileAtFirstChangedLine': '첫 변경 줄에서 이 파일 열기',
'diffView.hunk.label': '허크',
'diffView.hunk.stage': '스테이지',
'diffView.hunk.unstage': '스테이지 해제',
'diffView.hunk.discard': '취소',
'diffView.hunk.stageTitle': '허크 {index} 스테이지',
'diffView.hunk.unstageTitle': '허크 {index} 스테이지 해제',
'diffView.hunk.discardTitle': '허크 {index} 취소',
'diffView.hunk.unavailable': '이 허크는 더 이상 사용할 수 없습니다. diff를 새로고침 후 다시 시도하세요.',
'diffView.hunk.unsupported': '개별 허크 스테이징은 이 환경에서 지원되지 않습니다.',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '플랜',
'rightSidebar.contextNotesTodo.empty.selectProject': '메모와 Todo를 추가할 프로젝트를 선택하세요.',
'rightSidebar.contextNotesTodo.notes.title': '빠른 메모 - {project}',
+9
View File
@@ -1415,6 +1415,15 @@ export const dict: Record<I18nKey, string> = {
'diffView.actions.loadFullFiles': 'Wczytaj pełne pliki',
'diffView.actions.disableFullFiles': 'Nie wczytuj pełnych plików',
'diffView.actions.openFileAtFirstChangedLine': 'Otwórz plik na pierwszej zmienionej linii',
'diffView.hunk.label': 'Fragmenty',
'diffView.hunk.stage': 'Przygotuj',
'diffView.hunk.unstage': 'Cofnij',
'diffView.hunk.discard': 'Odrzuć',
'diffView.hunk.stageTitle': 'Przygotuj fragment {index}',
'diffView.hunk.unstageTitle': 'Cofnij przygotowanie fragmentu {index}',
'diffView.hunk.discardTitle': 'Odrzuć fragment {index}',
'diffView.hunk.unavailable': 'Ten fragment nie jest już dostępny. Odśwież diff i spróbuj ponownie.',
'diffView.hunk.unsupported': 'Przygotowywanie pojedynczych fragmentów nie jest obsługiwane w tym środowisku.',
'diffView.actions.openFileInEditorAtChange': 'Otwórz plik w edytorze na zmianie',
'diffView.actions.renderAnyway': 'Renderuj mimo to',
'diffView.actions.retry': 'Ponów',
@@ -1181,6 +1181,15 @@ export const dict: Record<I18nKey, string> = {
"diffView.actions.enableLineWrap": "Ativar ajuste de linha",
"diffView.actions.openFileInEditorAtChange": "Abrir este arquivo no editor nesta alteração",
"diffView.actions.openFileAtFirstChangedLine": "Abrir este arquivo na primeira linha alterada",
"diffView.hunk.label": "Trechos",
"diffView.hunk.stage": "Preparar",
"diffView.hunk.unstage": "Remover",
"diffView.hunk.discard": "Descartar",
"diffView.hunk.stageTitle": "Preparar trecho {index}",
"diffView.hunk.unstageTitle": "Remover trecho {index} da preparação",
"diffView.hunk.discardTitle": "Descartar trecho {index}",
"diffView.hunk.unavailable": "Este trecho não está mais disponível. Atualize o diff e tente novamente.",
"diffView.hunk.unsupported": "A preparação de trechos individuais não é suportada neste ambiente.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "Plano",
"rightSidebar.contextNotesTodo.empty.selectProject": "Selecione um projeto para adicionar notas e tarefas pendentes.",
"rightSidebar.contextNotesTodo.notes.title": "Notas rápidas — {project}",
+9
View File
@@ -1181,6 +1181,15 @@ export const dict: Record<I18nKey, string> = {
"diffView.actions.enableLineWrap": "Увімкнути перенос рядків",
"diffView.actions.openFileInEditorAtChange": "Відкрити цей файл у редакторі на зміні",
"diffView.actions.openFileAtFirstChangedLine": "Відкрити цей файл у першому зміненому рядку",
"diffView.hunk.label": "Шматки",
"diffView.hunk.stage": "Додати",
"diffView.hunk.unstage": "Прибрати",
"diffView.hunk.discard": "Відкинути",
"diffView.hunk.stageTitle": "Додати шматок {index} до індексу",
"diffView.hunk.unstageTitle": "Прибрати шматок {index} з індексу",
"diffView.hunk.discardTitle": "Відкинути шматок {index}",
"diffView.hunk.unavailable": "Цей шматок більше недоступний. Оновіть diff і спробуйте знову.",
"diffView.hunk.unsupported": "Додавання окремих шматків до індексу не підтримується в цьому середовищі.",
"rightSidebar.contextNotesTodo.plan.defaultTitle": "План",
"rightSidebar.contextNotesTodo.empty.selectProject": "Виберіть проєкт, щоб додати нотатки та завдання.",
"rightSidebar.contextNotesTodo.notes.title": "Швидкі нотатки - {project}",
@@ -1181,6 +1181,15 @@ export const dict: Record<I18nKey, string> = {
'diffView.actions.enableLineWrap': '开启自动换行',
'diffView.actions.openFileInEditorAtChange': '在编辑器中打开此文件并定位变更',
'diffView.actions.openFileAtFirstChangedLine': '在首个变更行打开此文件',
'diffView.hunk.label': '代码块',
'diffView.hunk.stage': '暂存',
'diffView.hunk.unstage': '取消暂存',
'diffView.hunk.discard': '放弃',
'diffView.hunk.stageTitle': '暂存代码块 {index}',
'diffView.hunk.unstageTitle': '取消暂存代码块 {index}',
'diffView.hunk.discardTitle': '放弃代码块 {index}',
'diffView.hunk.unavailable': '此代码块已不可用。请刷新差异后重试。',
'diffView.hunk.unsupported': '此运行环境不支持暂存单个代码块。',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '计划',
'rightSidebar.contextNotesTodo.empty.selectProject': '请选择一个项目以添加笔记和待办事项。',
'rightSidebar.contextNotesTodo.notes.title': '快速笔记 - {project}',
@@ -1191,6 +1191,15 @@ export const dict: Record<I18nKey, string> = {
'diffView.actions.enableLineWrap': '開啟自動換行',
'diffView.actions.openFileInEditorAtChange': '在編輯器中開啟此檔案並定位變更',
'diffView.actions.openFileAtFirstChangedLine': '在首個變更行開啟此檔案',
'diffView.hunk.label': '程式碼區塊',
'diffView.hunk.stage': '暫存',
'diffView.hunk.unstage': '取消暫存',
'diffView.hunk.discard': '捨棄',
'diffView.hunk.stageTitle': '暫存程式碼區塊 {index}',
'diffView.hunk.unstageTitle': '取消暫存程式碼區塊 {index}',
'diffView.hunk.discardTitle': '捨棄程式碼區塊 {index}',
'diffView.hunk.unavailable': '此程式碼區塊已不可用。請重新整理差異後重試。',
'diffView.hunk.unsupported': '此執行環境不支援暫存個別程式碼區塊。',
'rightSidebar.contextNotesTodo.plan.defaultTitle': '計畫',
'rightSidebar.contextNotesTodo.empty.selectProject': '請選擇一個專案以新增筆記和待辦事項。',
'rightSidebar.contextNotesTodo.notes.title': '快速筆記 - {project}',