Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7549f4e
cleaner navigation
dadukhankevin Feb 4, 2026
193755d
much cleaner, and normal, comments system (like discord)
dadukhankevin Feb 4, 2026
19a0009
Enhance notebookSafeSaveUtils with uriExistsWithFs function and updat…
BenjaminScholtens Feb 3, 2026
b626d16
Refactor VTT round-trip integration test to improve type safety and c…
BenjaminScholtens Feb 3, 2026
a5e5f15
Update import paths in VTT round-trip integration test and subtitles …
BenjaminScholtens Feb 3, 2026
8d7a004
Refactor error handling in atomicWriteUriTextWithFs to improve clarit…
BenjaminScholtens Feb 3, 2026
4490254
Improve atomicWriteUriTextWithFs by introducing a tempFileCreated fla…
BenjaminScholtens Feb 3, 2026
4e661b8
Enhance mediaStrategyFlags test by increasing timeout and improving c…
BenjaminScholtens Feb 3, 2026
e191c58
Update version in package.json from 0.16.0 to 0.17.0
BenjaminScholtens Feb 4, 2026
c561666
better nav and comments UI
dadukhankevin Feb 4, 2026
3ca9b04
Merge branch 'main' into daniel/cleaner_ui
BenjaminScholtens Feb 4, 2026
3a1e74d
Merge branch 'main' into daniel/cleaner_ui
LeviXIII Feb 10, 2026
6f4d1b7
Merge branch 'main' into daniel/cleaner_ui
LeviXIII Feb 10, 2026
2b355f5
futher improved UI in the project settings page
dadukhankevin Feb 11, 2026
46e3362
Merge branch 'main' into daniel/cleaner_ui
dadukhankevin Feb 11, 2026
fa5ed7e
some UI fixes
dadukhankevin Feb 12, 2026
580be58
- Adjust size and positioning of icons.
LeviXIII Feb 12, 2026
2386d7a
- Fix progress percentages when updating cells.
LeviXIII Feb 12, 2026
e57e7da
Merge branch 'main' into daniel/cleaner_ui
LeviXIII Feb 12, 2026
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
108 changes: 96 additions & 12 deletions sharedUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export const getCellValueData = (cell: QuillCellContent) => {
// Ensure editHistory exists and is an array
const editHistory = cell.editHistory || [];

// Find the latest edit that matches the current cell content
const latestEditThatMatchesCellValue = editHistory
.slice()
.reverse()
.find((edit) => EditMapUtils.isValue(edit.editMap) && edit.value === cell.cellContent);
// Find the latest edit that matches the current cell content (strict match).
// Falls back to the latest value edit if the strict match fails, which can happen
// when the merge step during save subtly normalizes the stored value.
const reversed = editHistory.slice().reverse();
const latestEditThatMatchesCellValue =
reversed.find((edit) => EditMapUtils.isValue(edit.editMap) && edit.value === cell.cellContent) ??
reversed.find((edit) => EditMapUtils.isValue(edit.editMap) && !edit.preview);

// Get audio validation from attachments instead of edits
let audioValidatedBy: ValidationEntry[] = [];
Expand Down Expand Up @@ -148,40 +150,122 @@ export const cellHasAudioUsingAttachments = (

if (selectedAudioId && atts[selectedAudioId]) {
const att = atts[selectedAudioId];
return att && att.type === "audio" && att.isDeleted === false && att.isMissing !== true;
return att && att.type === "audio" && !att.isDeleted && att.isMissing !== true;
}

return Object.values(atts).some(
(att: any) => att && att.type === "audio" && att.isDeleted === false && att.isMissing !== true
(att: any) => att && att.type === "audio" && !att.isDeleted && att.isMissing !== true
);
};

// Count only active (non-deleted) validation entries. Requires isDeleted === false so legacy
// or malformed entries (e.g. missing isDeleted) are not counted as validated.
export function countActiveValidations(validatedBy: ValidationEntry[] | undefined): number {
return (validatedBy?.filter((v) => v && typeof v === "object" && v.isDeleted === false).length ?? 0);
}

export const computeValidationStats = (
cellValueData: Array<{ validatedBy?: ValidationEntry[]; audioValidatedBy?: ValidationEntry[]; }>,
cellValueData: Array<{
validatedBy?: ValidationEntry[];
audioValidatedBy?: ValidationEntry[];
cellContent?: string;
}>,
minimumValidationsRequired: number,
minimumAudioValidationsRequired: number
): {
validatedCells: number;
audioValidatedCells: number;
fullyValidatedCells: number;
} => {
// Only count a cell as text-validated if it has actual text content. Empty/placeholder
// cells should not inflate validation % when no validations are meaningfully applied.
const validatedCells = cellValueData.filter((cell) => {
return (cell.validatedBy?.filter((v) => !v.isDeleted).length || 0) >= minimumValidationsRequired;
if (!hasTextContent(cell.cellContent)) return false;
return countActiveValidations(cell.validatedBy) >= minimumValidationsRequired;
}).length;

const audioValidatedCells = cellValueData.filter((cell) => {
return (cell.audioValidatedBy?.filter((v) => !v.isDeleted).length || 0) >= minimumAudioValidationsRequired;
return countActiveValidations(cell.audioValidatedBy) >= minimumAudioValidationsRequired;
}).length;

const fullyValidatedCells = cellValueData.filter((cell) => {
const textOk = (cell.validatedBy?.filter((v) => !v.isDeleted).length || 0) >= minimumValidationsRequired;
const audioOk = (cell.audioValidatedBy?.filter((v) => !v.isDeleted).length || 0) >= minimumAudioValidationsRequired;
const textOk =
hasTextContent(cell.cellContent) &&
countActiveValidations(cell.validatedBy) >= minimumValidationsRequired;
const audioOk = countActiveValidations(cell.audioValidatedBy) >= minimumAudioValidationsRequired;
return textOk && audioOk;
}).length;

return { validatedCells, audioValidatedCells, fullyValidatedCells };
};

/**
* Cell-like shape used for progress exclusion checks (notebook cell or serialized cell).
*/
export type CellForProgressCheck = {
metadata?: {
id?: string;
type?: string;
parentId?: string;
data?: { merged?: boolean; parentId?: string; type?: string; };
};
};

/**
* Returns true if the cell should be excluded from progress (not counted in totalCells).
* Paratext and child cells (e.g. type "text" with parentId) must not count toward progress.
*/
export function shouldExcludeCellFromProgress(cell: CellForProgressCheck): boolean {
const md = cell.metadata;
const cellData = md?.data as { merged?: boolean; parentId?: string; type?: string; } | undefined;
const cellId = (md?.id ?? "").toString();

if (md?.type === "milestone" || cellData?.merged) {
return true;
}
const isParatext =
md?.type === "paratext" ||
cellData?.type === "paratext" ||
cellId.includes("paratext-");
if (isParatext) {
return true;
}
if (!cellId || cellId.trim() === "") {
return true;
}
const parentId = md?.parentId ?? cellData?.parentId;
if (parentId != null && parentId !== "") {
return true;
}
return false;
}

/**
* Returns true if the cell should be excluded from progress when already in QuillCellContent form.
* Use this to filter lists before computing validation stats so paratext/child never count.
*/
export function shouldExcludeQuillCellFromProgress(cell: QuillCellContent): boolean {
const cellId = (cell.cellMarkers?.[0] ?? "").toString();
if (!cellId || cellId.trim() === "") {
return true;
}
if (cell.merged) {
return true;
}
const typeLower = (cell.cellType ?? "").toString().toLowerCase();
if (typeLower === "milestone") {
return true;
}
if (typeLower === "paratext" || cellId.includes("paratext-")) {
return true;
}
const parentId = cell.metadata?.parentId ?? cell.data?.parentId;
if (parentId != null && parentId !== "") {
return true;
}
return false;
}

export const computeProgressPercents = (
totalCells: number,
cellsWithValues: number,
Expand Down
59 changes: 26 additions & 33 deletions src/providers/codexCellEditorProvider/codexDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { randomUUID } from "crypto";
import { CodexContentSerializer } from "../../serializer";
import { debounce } from "lodash";
import { getSQLiteIndexManager } from "../../activationHelpers/contextAware/contentIndexes/indexes/sqliteIndexManager";
import { getCellValueData, cellHasAudioUsingAttachments, computeValidationStats, computeProgressPercents } from "../../../sharedUtils";
import { getCellValueData, cellHasAudioUsingAttachments, computeValidationStats, computeProgressPercents, shouldExcludeCellFromProgress, shouldExcludeQuillCellFromProgress, countActiveValidations, hasTextContent } from "../../../sharedUtils";
import { extractParentCellIdFromParatext, convertCellToQuillContent } from "./utils/cellUtils";
import { formatJsonForNotebookFile, normalizeNotebookFileText } from "../../utils/notebookFileFormattingUtils";
import { atomicWriteUriText, readExistingFileOrThrow } from "../../utils/notebookSafeSaveUtils";
Expand Down Expand Up @@ -1551,24 +1551,19 @@ export class CodexCellDocument implements vscode.CustomDocument {
const cellsForMilestone: QuillCellContent[] = [];
for (let j = startIndex; j < endIndex; j++) {
const cell = cells[j];

// Skip milestone cells
if (cell.metadata?.type === CodexCellTypes.MILESTONE) {
continue;
}

// Skip paratext and merged cells
const cellId = cell.metadata?.id;
if (!cellId || cellId.includes(":paratext-") || cell.metadata?.type === CodexCellTypes.PARATEXT || cell.metadata?.data?.merged) {
if (shouldExcludeCellFromProgress(cell)) {
continue;
}

// Convert to QuillCellContent format
const quillContent = convertCellToQuillContent(cell);
cellsForMilestone.push(quillContent);
}

const totalCells = cellsForMilestone.length;
// Only root content cells count for progress (exclude paratext/child again for validation)
const progressCells = cellsForMilestone.filter(
(c) => !shouldExcludeQuillCellFromProgress(c)
);
const totalCells = progressCells.length;
if (totalCells === 0) {
// Milestone number is 1-based (i + 1)
progress[i + 1] = {
Expand All @@ -1582,23 +1577,23 @@ export class CodexCellDocument implements vscode.CustomDocument {
}

// Count cells with content (translated)
const cellsWithValues = cellsForMilestone.filter(
const cellsWithValues = progressCells.filter(
(cell) =>
cell.cellContent &&
cell.cellContent.trim().length > 0 &&
cell.cellContent !== "<span></span>"
).length;

// Count cells with audio
const cellsWithAudioValues = cellsForMilestone.filter((cell) =>
const cellsWithAudioValues = progressCells.filter((cell) =>
cellHasAudioUsingAttachments(
cell.attachments,
cell.metadata?.selectedAudioId
)
).length;

// Calculate validation data
const cellWithValidatedData = cellsForMilestone.map((cell) => getCellValueData(cell));
// Calculate validation data (only from root content cells)
const cellWithValidatedData = progressCells.map((cell) => getCellValueData(cell));

const { validatedCells, audioValidatedCells, fullyValidatedCells } =
computeValidationStats(
Expand Down Expand Up @@ -1681,18 +1676,9 @@ export class CodexCellDocument implements vscode.CustomDocument {
const contentCells: QuillCellContent[] = [];
for (let i = startCellIndex; i < endCellIndex; i++) {
const cell = cells[i];

// Skip milestone cells
if (cell.metadata?.type === CodexCellTypes.MILESTONE) {
continue;
}

// Skip paratext and merged cells
const cellId = cell.metadata?.id;
if (!cellId || cellId.includes(":paratext-") || cell.metadata?.type === CodexCellTypes.PARATEXT || cell.metadata?.data?.merged) {
if (shouldExcludeCellFromProgress(cell)) {
continue;
}

// Convert to QuillCellContent format
const quillContent = convertCellToQuillContent(cell);
contentCells.push(quillContent);
Expand Down Expand Up @@ -1736,7 +1722,11 @@ export class CodexCellDocument implements vscode.CustomDocument {
contentCellIdsForSubsection.has(c.cellMarkers[0])
);

const totalCells = subsectionCells.length;
// Only root content cells count for progress (exclude paratext/child for validation)
const progressCells = subsectionCells.filter(
(c) => !shouldExcludeQuillCellFromProgress(c)
);
const totalCells = progressCells.length;
if (totalCells === 0) {
progress[subsectionIdx] = {
percentTranslationsCompleted: 0,
Expand All @@ -1753,23 +1743,23 @@ export class CodexCellDocument implements vscode.CustomDocument {
}

// Count cells with content (translated)
const cellsWithValues = subsectionCells.filter(
const cellsWithValues = progressCells.filter(
(cell) =>
cell.cellContent &&
cell.cellContent.trim().length > 0 &&
cell.cellContent !== "<span></span>"
).length;

// Count cells with audio
const cellsWithAudioValues = subsectionCells.filter((cell) =>
const cellsWithAudioValues = progressCells.filter((cell) =>
cellHasAudioUsingAttachments(
cell.attachments,
cell.metadata?.selectedAudioId
)
).length;

// Calculate validation data
const cellWithValidatedData = subsectionCells.map((cell) => getCellValueData(cell));
// Calculate validation data (only from root content cells)
const cellWithValidatedData = progressCells.map((cell) => getCellValueData(cell));

const { validatedCells, audioValidatedCells, fullyValidatedCells } =
computeValidationStats(
Expand All @@ -1778,9 +1768,12 @@ export class CodexCellDocument implements vscode.CustomDocument {
minimumAudioValidationsRequired
);

// Compute per-level validation percentages for text and audio
// Compute per-level validation percentages for text and audio.
// For text, only count validations on cells with actual content (same rule as computeValidationStats).
const countNonDeleted = (arr: any[] | undefined) => (arr || []).filter((v: any) => !v.isDeleted).length;
const textValidationCounts = cellWithValidatedData.map((c) => countNonDeleted(c.validatedBy));
const textValidationCounts = cellWithValidatedData.map((c) =>
hasTextContent(c.cellContent) ? countActiveValidations(c.validatedBy) : 0
);
const audioValidationCounts = cellWithValidatedData.map((c) => countNonDeleted(c.audioValidatedBy));

const computeLevelPercents = (counts: number[], maxLevel: number) => {
Expand Down
62 changes: 61 additions & 1 deletion src/providers/mainMenu/mainMenuProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from "vscode";
import { getProjectOverview, findAllCodexProjects, checkIfMetadataAndGitIsInitialized, extractProjectIdFromFolderName } from "../../projectManager/utils/projectUtils";
import { getProjectOverview, findAllCodexProjects, checkIfMetadataAndGitIsInitialized, extractProjectIdFromFolderName, disableSyncTemporarily } from "../../projectManager/utils/projectUtils";
import { getAuthApi } from "../../extension";
import { openSystemMessageEditor } from "../../copilotSettings/copilotSettings";
import { openProjectExportView } from "../../projectManager/projectExportView";
Expand Down Expand Up @@ -617,6 +617,12 @@ export class MainMenuProvider extends BaseWebviewProvider {
// For backward compatibility, redirect to setValidationCount
await this.executeCommandAndNotify("setValidationCount");
break;
case "setValidationCountDirect":
await this.handleSetValidationCountDirect(message.data?.count, "validationCount");
break;
case "setValidationCountAudioDirect":
await this.handleSetValidationCountDirect(message.data?.count, "validationCountAudio");
break;
case "openEditAnalysis":
await vscode.commands.executeCommand("codex-editor-extension.analyzeEdits");
break;
Expand Down Expand Up @@ -1777,6 +1783,60 @@ export class MainMenuProvider extends BaseWebviewProvider {
}
}

private async handleSetValidationCountDirect(count: number | undefined, configKey: "validationCount" | "validationCountAudio"): Promise<void> {
if (count === undefined || count < 1 || count > 15) return;

const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceFolder) return;

disableSyncTemporarily();

const config = vscode.workspace.getConfiguration("codex-project-manager");
await config.update(configKey, count, vscode.ConfigurationTarget.Workspace);

let author = "unknown";
try {
const authApi = await getAuthApi();
const userInfo = await authApi?.getUserInfo();
if (userInfo?.username) {
author = userInfo.username;
}
} catch (_) {
// Silent fallback
}

const result = await MetadataManager.safeUpdateMetadata(
workspaceFolder,
(project: Record<string, unknown>) => {
const meta = (project.meta as Record<string, unknown>) || {};
const original = meta[configKey];
meta[configKey] = count;
project.meta = meta;

if (original !== count) {
if (!project.edits) {
project.edits = [];
}
addProjectMetadataEdit(
project as Parameters<typeof addProjectMetadataEdit>[0],
EditMapUtils.metaField(configKey),
count,
author,
);
}
return project;
},
{ author },
);

if (!result.success) {
console.error("Failed to update metadata:", result.error);
}

await this.store.refreshState();
await this.updateProjectOverview();
}

private async handleApplyTextDisplaySettings(settings: any): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
Expand Down
Loading