Hardening + collaboration features from full app review

Security and robustness (backend):
- Strict CSP (was null); confine read_file/launch_file/ue_log to the client root
- p4 trust without -f: surface fingerprint CHANGES instead of auto-accepting (MITM)
- stdin=null by default + non-interactive P4EDITOR so terminal commands never hang
- 64 MB read caps on read_depot/read_file/print_depot (no OOM on huge .uasset/.pak)
- Recover from mutex poisoning instead of crashing every later p4 call
- Decode p4 output as UTF-8 then cp1251 (Cyrillic paths no longer corrupt)
- p4_changes never deletes changelists that hold shelved files
- Validate changelist number in p4_undo_change

Bug fixes (frontend):
- Focus-refresh preserves the checkbox selection (never re-checks deselected files)
- Staleness guards on refresh/describe/AI-summary/user-groups (no cross-selection races)
- Real F5/Ctrl+R/G/S/B handlers; match e.code so Ctrl+backquote works on RU layout
- Confirm dialog no longer fires on a stray global Enter
- Panel/dock/history drags end on window blur (no stuck resize)
- Commit draft clears on Submit only if unchanged since committing
- Summary/IDE buttons gated to real source files (not .pak/.dll/binaries)

New features:
- Exclusive lock/unlock plus held-by/locked-by indicators (p4 opened -a)
- Shelve/unshelve/delete-shelf on pending changelists
- Resolve UI (auto-merge / accept yours / accept theirs, per file or all) + banner
- Diff a history revision against the previous one; blame/annotate view
- Depot file search (Tools -> Search depot)
- Move opened files between pending changelists
- New-submit toasts from teammates + offline banner with auto-reconnect

Also: bake OpenRouter key at build time (git-ignored), drop AI settings UI,
IDE picker (detect installed editors), AI replies in the UI language,
persistent code summaries, metadata/CSP polish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-07 21:26:57 +03:00
parent 8e2cac4437
commit 6c16d4a4f4
12 changed files with 1487 additions and 163 deletions

View File

@ -85,7 +85,8 @@ export const p4 = {
findSln: (scope: string) => invoke<string>("find_sln", { scope }),
buildSln: (path: string, config: string) => invoke<string>("build_sln", { path, config }),
launchFile: (path: string) => invoke<void>("launch_file", { path }),
openInVscode: (depot: string) => invoke<void>("open_in_vscode", { depot }),
listEditors: () => invoke<Editor[]>("list_editors"),
openInEditor: (depot: string, editor: string) => invoke<void>("open_in_editor", { depot, editor }),
readDepot: async (depot: string): Promise<ArrayBuffer> => {
const r: unknown = await invoke("read_depot", { depot });
if (r instanceof ArrayBuffer) return r;
@ -107,10 +108,35 @@ export const p4 = {
groupMember: (group: string, user: string, add: boolean) => invoke<string>("p4_group_member", { group, user, add }),
readAiKey: () => invoke<string>("read_ai_key"),
saveAiKey: (key: string) => invoke<void>("save_ai_key", { key }),
// run an arbitrary p4 command in the built-in terminal (combined stdout+stderr)
console: (args: string[]) => invoke<string>("p4_console", { args }),
// tail the newest Unreal log for a project (.uproject path → Saved/Logs/*.log)
ueLog: (uproject: string, tailBytes = 200_000) => invoke<string>("ue_log", { uproject, tailBytes }),
// --- collaboration / advanced Perforce ---
openedAll: (scope = "") => invoke<OpenedFile[]>("p4_opened_all", { scope }),
lock: (files: string[]) => invoke<string>("p4_lock", { files }),
unlock: (files: string[]) => invoke<string>("p4_unlock", { files }),
shelve: (change: string) => invoke<string>("p4_shelve", { change }),
unshelve: (change: string) => invoke<string>("p4_unshelve", { change }),
deleteShelf: (change: string) => invoke<string>("p4_delete_shelf", { change }),
shelvedFiles: (change: string) => invoke<Describe>("p4_shelved_files", { change }),
resolveList: () => invoke<OpenedFile[]>("p4_resolve_list"),
resolveFile: (file: string, mode: "am" | "ay" | "at") => invoke<string>("p4_resolve_file", { file, mode }),
diff2: (spec1: string, spec2: string) => invoke<string>("p4_diff2", { spec1, spec2 }),
annotate: (spec: string) => invoke<string>("p4_annotate", { spec }),
searchFiles: (query: string, scope = "") => invoke<{ depotFile?: string; rev?: string }[]>("p4_search_files", { query, scope }),
reopenTo: (change: string, files: string[]) => invoke<string>("p4_reopen_to", { change, files }),
latestChange: (scope = "") => invoke<Change | null>("p4_latest_change", { scope }),
};
export interface User { User?: string; Email?: string; FullName?: string; Access?: string; Update?: string; [k: string]: unknown }
// a detected code editor / IDE
export interface Editor { id: string; name: string; exe: string; args: string[] }
// which editor to open code files in (persisted; "" → auto-pick)
export function getEditor(): string { try { return localStorage.getItem("exd-editor") || ""; } catch { return ""; } }
export function setEditorPref(id: string) { try { localStorage.setItem("exd-editor", id); } catch {} }
export interface Dir { dir?: string; [k: string]: unknown }
// depot path -> short leaf name (last segment)
export function leaf(path?: string): string {
@ -186,3 +212,13 @@ export function kindOf(name: string): Kind {
if (/\.(png|jpe?g|webp|gif|bmp|svg)$/.test(n)) return "image";
return "code";
}
// true only for real text/source files — gates the "Summary" and "Open in IDE"
// buttons so they never appear on binaries (.pak/.dll/.exe/.bin/etc.) that
// happen to fall through kindOf() as "code".
const CODE_EXT = /\.(c|cc|cpp|cxx|h|hpp|hh|inl|cs|ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|kt|kts|swift|m|mm|rb|php|lua|sh|bash|ps1|bat|cmd|sql|json|jsonc|yaml|yml|toml|ini|cfg|conf|xml|html|htm|css|scss|sass|less|md|markdown|txt|log|csv|gitignore|editorconfig|uproject|uplugin|build|target|usf|ush|hlsl|glsl|shader|cginc|props|targets|sln|vcxproj|csproj|gradle|cmake|make|mk)$/i;
export function isCodeFile(name: string): boolean {
const n = name.toLowerCase();
if (/\.(uasset|umap|pak|exe|dll|so|dylib|bin|obj|lib|a|pdb|zip|7z|rar|gz|tar|png|jpe?g|gif|bmp|webp|svg|fbx|obj|gltf|glb|stl|ply|dae|wav|mp3|ogg|mp4|mov|ttf|otf)$/i.test(n)) return false;
return CODE_EXT.test(n);
}