Merge remote-tracking branch 'origin/main'

This commit is contained in:
Bohdan Triapitsyn
2026-06-14 14:52:37 +03:00
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";
@@ -389,51 +390,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 {
@@ -481,6 +498,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);
@@ -601,6 +619,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">
@@ -755,6 +780,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}',