mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Resolve settings display and selection diff design issues
This commit is contained in:
parent
70bcd45e5b
commit
7c86d89810
4 changed files with 79 additions and 14 deletions
|
|
@ -14,6 +14,8 @@ export function buildSelectionUnifiedDiff(filePath: string, originalText: string
|
|||
].join("\n");
|
||||
}
|
||||
|
||||
const MAX_SELECTION_REWRITE_LCS_CELLS = 40_000;
|
||||
|
||||
interface DiffLine {
|
||||
prefix: " " | "+" | "-";
|
||||
text: string;
|
||||
|
|
@ -27,6 +29,10 @@ function textLines(text: string): string[] {
|
|||
}
|
||||
|
||||
function lineChanges(originalLines: string[], replacementLines: string[]): DiffLine[] {
|
||||
if (originalLines.length * replacementLines.length > MAX_SELECTION_REWRITE_LCS_CELLS) {
|
||||
return linearLineChanges(originalLines, replacementLines);
|
||||
}
|
||||
|
||||
const lengths = lcsLengths(originalLines, replacementLines);
|
||||
const changes: DiffLine[] = [];
|
||||
let oldIndex = 0;
|
||||
|
|
@ -52,6 +58,45 @@ function lineChanges(originalLines: string[], replacementLines: string[]): DiffL
|
|||
return changes.length > 0 ? changes : [{ prefix: " ", text: "" }];
|
||||
}
|
||||
|
||||
function linearLineChanges(originalLines: string[], replacementLines: string[]): DiffLine[] {
|
||||
const prefixLength = commonPrefixLength(originalLines, replacementLines);
|
||||
const suffixLength = commonSuffixLength(originalLines, replacementLines, prefixLength);
|
||||
const changes: DiffLine[] = [];
|
||||
|
||||
pushLines(changes, " ", originalLines, 0, prefixLength);
|
||||
pushLines(changes, "-", originalLines, prefixLength, originalLines.length - suffixLength);
|
||||
pushLines(changes, "+", replacementLines, prefixLength, replacementLines.length - suffixLength);
|
||||
pushLines(changes, " ", originalLines, originalLines.length - suffixLength, originalLines.length);
|
||||
|
||||
return changes.length > 0 ? changes : [{ prefix: " ", text: "" }];
|
||||
}
|
||||
|
||||
function commonPrefixLength(left: string[], right: string[]): number {
|
||||
let prefixLength = 0;
|
||||
while (prefixLength < left.length && prefixLength < right.length && left[prefixLength] === right[prefixLength]) {
|
||||
prefixLength += 1;
|
||||
}
|
||||
return prefixLength;
|
||||
}
|
||||
|
||||
function commonSuffixLength(left: string[], right: string[], prefixLength: number): number {
|
||||
let suffixLength = 0;
|
||||
while (
|
||||
suffixLength < left.length - prefixLength &&
|
||||
suffixLength < right.length - prefixLength &&
|
||||
left[left.length - 1 - suffixLength] === right[right.length - 1 - suffixLength]
|
||||
) {
|
||||
suffixLength += 1;
|
||||
}
|
||||
return suffixLength;
|
||||
}
|
||||
|
||||
function pushLines(changes: DiffLine[], prefix: DiffLine["prefix"], lines: string[], start: number, end: number): void {
|
||||
for (let index = start; index < end; index += 1) {
|
||||
changes.push({ prefix, text: lines[index] ?? "" });
|
||||
}
|
||||
}
|
||||
|
||||
function lcsLengths(left: string[], right: string[]): number[][] {
|
||||
const rows = Array.from({ length: left.length + 1 }, () => Array.from({ length: right.length + 1 }, () => 0));
|
||||
for (let leftIndex = left.length - 1; leftIndex >= 0; leftIndex -= 1) {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,10 @@ import {
|
|||
} from "./lifecycle";
|
||||
|
||||
interface SettingsDynamicSectionsControllerCallbacks {
|
||||
display(target: SettingsDynamicSectionsDisplayTarget): void;
|
||||
display(): void;
|
||||
notify(message: string): void;
|
||||
}
|
||||
|
||||
type SettingsDynamicSectionsDisplayTarget = "all" | "helper" | "archived" | "hooks";
|
||||
|
||||
export interface SettingsDynamicSectionsSnapshot {
|
||||
archivedThreads: readonly Thread[];
|
||||
archivedThreadsLifecycle: SettingsDynamicSectionLifecycleState;
|
||||
|
|
@ -119,7 +117,7 @@ export class SettingsDynamicSectionsController {
|
|||
const observedModels = observedValue(result);
|
||||
if (!observedModels) return;
|
||||
this.models = [...observedModels];
|
||||
this.callbacks.display("helper");
|
||||
this.callbacks.display();
|
||||
}
|
||||
|
||||
private receiveObservedArchivedThreadsResult(result: ObservedResult<readonly Thread[]>): void {
|
||||
|
|
@ -134,7 +132,7 @@ export class SettingsDynamicSectionsController {
|
|||
operationToken: this.archivedThreadsOperationToken,
|
||||
});
|
||||
}
|
||||
this.callbacks.display("archived");
|
||||
this.callbacks.display();
|
||||
}
|
||||
|
||||
async refreshDynamicSections(options: { forceModels?: boolean } = {}): Promise<void> {
|
||||
|
|
@ -158,7 +156,7 @@ export class SettingsDynamicSectionsController {
|
|||
status: "Loading hooks...",
|
||||
operationToken: hooksOperationToken,
|
||||
});
|
||||
this.callbacks.display("all");
|
||||
this.callbacks.display();
|
||||
|
||||
let failedCount = 0;
|
||||
try {
|
||||
|
|
@ -261,7 +259,7 @@ export class SettingsDynamicSectionsController {
|
|||
if (failedCount > 0) {
|
||||
this.callbacks.notify("Could not refresh all Codex details.");
|
||||
}
|
||||
this.callbacks.display("all");
|
||||
this.callbacks.display();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -435,7 +433,7 @@ export class SettingsDynamicSectionsController {
|
|||
operationToken,
|
||||
}),
|
||||
);
|
||||
this.callbacks.display(displayTargetForDynamicOperationSection(options.section));
|
||||
this.callbacks.display();
|
||||
try {
|
||||
await options.operation(operationToken);
|
||||
} catch (error) {
|
||||
|
|
@ -449,7 +447,7 @@ export class SettingsDynamicSectionsController {
|
|||
);
|
||||
this.callbacks.notify(options.failureNotice);
|
||||
} finally {
|
||||
if (!stale()) this.callbacks.display(displayTargetForDynamicOperationSection(options.section));
|
||||
if (!stale()) this.callbacks.display();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -490,10 +488,6 @@ export class SettingsDynamicSectionsController {
|
|||
}
|
||||
}
|
||||
|
||||
function displayTargetForDynamicOperationSection(section: "hooks" | "archivedThreads"): SettingsDynamicSectionsDisplayTarget {
|
||||
return section === "hooks" ? "hooks" : "archived";
|
||||
}
|
||||
|
||||
function archivedThreadsStatus(count: number): string {
|
||||
return `Loaded ${String(count)} archived thread${count === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,32 @@ describe("selection rewrite diff", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("bounds large selection diffs to a linear prefix and suffix comparison", () => {
|
||||
const originalText = [
|
||||
"same start",
|
||||
...Array.from({ length: 160 }, (_unused, index) => `old before ${String(index)}`),
|
||||
"shared middle",
|
||||
...Array.from({ length: 160 }, (_unused, index) => `old after ${String(index)}`),
|
||||
"same end",
|
||||
].join("\n");
|
||||
const replacementText = [
|
||||
"same start",
|
||||
...Array.from({ length: 160 }, (_unused, index) => `new before ${String(index)}`),
|
||||
"shared middle",
|
||||
...Array.from({ length: 160 }, (_unused, index) => `new after ${String(index)}`),
|
||||
"same end",
|
||||
].join("\n");
|
||||
|
||||
const diff = buildSelectionUnifiedDiff("Note.md", originalText, replacementText);
|
||||
|
||||
expect(diff).toContain("\n same start\n");
|
||||
expect(diff).toContain("\n-old before 0\n");
|
||||
expect(diff).toContain("\n-shared middle\n");
|
||||
expect(diff).toContain("\n+shared middle\n");
|
||||
expect(diff).toContain("\n same end");
|
||||
expect(diff).not.toContain("\n shared middle\n");
|
||||
});
|
||||
|
||||
it("renders additions and deletions", () => {
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "", "added")).toContain("+added");
|
||||
expect(buildSelectionUnifiedDiff("Note.md", "removed", "")).toContain("-removed");
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ describe("settings tab", () => {
|
|||
|
||||
expect(controller.snapshot().archivedThreads.map((thread) => thread.preview)).toEqual(["Archived elsewhere"]);
|
||||
expect(controller.snapshot().archivedThreadsLifecycle.kind).toBe("loaded");
|
||||
expect(display).toHaveBeenCalledWith("archived");
|
||||
expect(display).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("ignores stale dynamic sections refresh results after a newer refresh completes", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue