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

@ -363,6 +363,38 @@ body.resizing{cursor:col-resize!important;user-select:none}
.logcount{margin-left:auto;flex:0 0 auto;color:var(--faint);white-space:nowrap}
.logrow.err .logcount{color:var(--del)}
/* bottom dock — tabbed Log / Terminal / Unreal */
.dockpanel{position:relative;flex:0 0 240px;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--border);background:var(--sunk)}
.dockhandle{position:absolute;top:-3px;left:0;right:0;height:7px;cursor:row-resize;z-index:9}
.dockhandle:hover{background:var(--accent);opacity:.5}
body.resizing-v{cursor:row-resize!important;user-select:none}
.dockhead{flex:0 0 auto;display:flex;align-items:center;gap:6px;padding:4px 10px 0;border-bottom:1px solid var(--border-soft)}
.docktabs{display:flex;gap:2px}
.docktab{display:flex;align-items:center;gap:6px;border:none;background:none;color:var(--muted);font-family:var(--font);font-size:12px;cursor:pointer;padding:7px 12px;border-radius:8px 8px 0 0;border-bottom:2px solid transparent;transition:.12s}
.docktab svg{width:14px;height:14px}
.docktab:hover{color:var(--txt);background:var(--hover)}
.docktab.on{color:var(--txt);border-bottom-color:var(--accent)}
.docktab.on svg{color:var(--accent-2)}
.dockspace{flex:1}
.dockpanel .loghbtn{margin-left:0}
/* terminal */
.term{flex:1;display:flex;flex-direction:column;min-height:0}
.termbody{flex:1;overflow-y:auto;min-height:0;padding:8px 12px;font-family:var(--mono);font-size:12px;line-height:1.5}
.termhint{color:var(--faint);font-family:var(--font);font-size:12px;padding:8px 2px;line-height:1.55}
.termhint.err{color:var(--del)}
.termblock{margin-bottom:9px}
.termcmd{color:var(--txt);white-space:pre-wrap;word-break:break-all}
.termprompt{color:var(--accent);font-weight:700;user-select:none;margin-right:3px}
.termout{margin:2px 0 0;white-space:pre-wrap;word-break:break-word;color:var(--muted)}
.termout.err{color:var(--del)}
.termout.run{color:var(--faint);display:flex;align-items:center;gap:8px}
.terminput-wrap{position:relative;flex:0 0 auto;display:flex;align-items:center;gap:6px;padding:9px 12px;border-top:1px solid var(--border-soft);background:var(--panel)}
.terminput{flex:1;background:none;border:none;outline:none;color:var(--txt);font-family:var(--mono);font-size:12.5px}
.termsug{position:absolute;left:32px;bottom:calc(100% - 1px);min-width:210px;max-height:220px;overflow-y:auto;background:var(--elevated);border:1px solid var(--border);border-radius:10px;box-shadow:0 -14px 34px -14px rgba(0,0,0,.65);z-index:20;padding:4px}
.termsug-i{padding:6px 10px;font-family:var(--mono);font-size:12px;color:var(--muted);border-radius:7px;cursor:pointer;white-space:nowrap}
.termsug-i b{color:var(--accent-2)}
.termsug-i.on,.termsug-i:hover{background:rgba(124,110,246,.14);color:var(--txt)}
/* animated startup splash */
.splash{display:flex;flex-direction:column;align-items:center;gap:16px;padding:20px;animation:updin .5s ease}
.splash-logo{width:64px;height:64px;border-radius:18px;display:grid;place-items:center;color:#fff;
@ -465,6 +497,14 @@ body.resizing{cursor:col-resize!important;user-select:none}
.gbtn:hover{color:var(--txt);border-color:var(--accent)}.gbtn svg{width:14px;height:14px}
.gbtn.vsc svg{color:#3aa0ff}
.gbtn.vsc:hover{border-color:#3aa0ff;color:#3aa0ff}
.gbtn.icon{padding:7px;width:34px;height:32px;justify-content:center;gap:0}
.gbtn.icon svg{width:16px;height:16px}
/* code-editor picker in Settings */
.edsel{flex:none;min-width:170px;font-size:12.5px;font-family:var(--font);color:var(--txt);
background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:7px 10px;cursor:pointer;transition:.15s}
.edsel:hover{border-color:var(--accent)}
.edsel:focus{outline:none;border-color:var(--accent)}
.edsel option{background:var(--panel);color:var(--txt)}
.asset{flex:1;min-height:0;display:grid;grid-template-rows:1fr auto}
.stage{position:relative;min-height:0;overflow:hidden;display:grid;place-items:center;
@ -567,9 +607,9 @@ body.resizing{cursor:col-resize!important;user-select:none}
/* History = GitHub-style split: changed-files list beside the file content (no covering) */
.histsplit{display:flex;min-width:0;min-height:0;overflow:hidden}
.histsplit>.right{flex:1 1 auto;min-width:0}
/* explicit width via a state-driven CSS variable */
.histsplit>.histlist{flex:none;width:var(--hist-w,340px);min-width:0;border-right:1px solid var(--border)}
.histsplit.solo>.histlist{flex:1 1 auto;width:auto;border-right:none} /* nothing open → list fills the panel */
/* width comes from a direct inline style on the element (set in ChangeDetail) */
.histsplit>.histlist{min-width:0;border-right:1px solid var(--border)}
.histsplit.solo>.histlist{flex:1 1 auto;border-right:none} /* nothing open → list fills the panel */
/* plain in-flow 8px column between list and preview — only the cursor hints it's draggable.
position MUST override the base .vhandle{position:absolute}, or it falls out of the flex flow */
.histsplit>.histh{position:relative;flex:none;width:8px;align-self:stretch;cursor:col-resize;background:transparent;top:auto;bottom:auto}
@ -673,6 +713,60 @@ body.resizing{cursor:col-resize!important;user-select:none}
.aibtn svg{width:15px;height:15px}
.aibtn.busy{color:var(--faint)}
.aibtn.icon{padding:0;width:34px;justify-content:center}
/* code "Summary" button + AI explanation card in the preview header */
.gbtn.ai{color:var(--accent-2);border-color:rgba(124,110,246,.35);background:linear-gradient(135deg,rgba(124,110,246,.14),rgba(124,110,246,.05))}
.gbtn.ai:hover{filter:brightness(1.1);border-color:rgba(124,110,246,.55)}
.gbtn.ai.busy{color:var(--faint)}
.explain{flex:0 0 auto;display:flex;align-items:flex-start;gap:10px;margin:0;padding:12px 16px;
background:linear-gradient(135deg,rgba(124,110,246,.1),rgba(124,110,246,.03));border-bottom:1px solid var(--border-soft)}
.explain-ic{flex:0 0 auto;color:var(--accent-2);display:flex}
.explain-ic svg{width:17px;height:17px}
.explain-body{flex:1;font-size:12.5px;line-height:1.55;color:var(--txt);min-width:0}
.explain-load{display:flex;align-items:center;gap:8px;color:var(--faint)}
.explain-x{flex:0 0 auto;border:none;background:none;color:var(--faint);font-size:18px;line-height:1;cursor:pointer;padding:0 2px}
.explain-x:hover{color:var(--txt)}
/* ---------------- collaboration UI (locks / banners / search / blame / diff / resolve) ---------------- */
/* connection + resolve banners */
.netbar{display:flex;align-items:center;gap:10px;padding:8px 16px;font-size:12.5px;font-weight:600;flex:0 0 auto}
.netbar svg{width:16px;height:16px;flex:0 0 auto}
.netbar.off{background:rgba(242,99,126,.14);color:var(--del);border-bottom:1px solid rgba(242,99,126,.3)}
.netbar.warn{background:rgba(232,176,75,.14);color:var(--edit);border-bottom:1px solid rgba(232,176,75,.3);cursor:pointer}
.netbar.warn:hover{background:rgba(232,176,75,.2)}
.netbar-btn{margin-left:auto;border:1px solid currentColor;background:none;color:inherit;font-size:11.5px;font-weight:700;
border-radius:7px;padding:4px 12px;cursor:pointer;font-family:var(--font)}
/* "held by someone else" pill on a Changes row */
.held{display:flex;align-items:center;gap:4px;flex:0 0 auto;font-size:10.5px;font-weight:600;color:var(--muted);
background:var(--chip);border:1px solid var(--border-soft);border-radius:7px;padding:2px 7px;max-width:120px;overflow:hidden}
.held svg{width:12px;height:12px;flex:0 0 auto}
.held span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.held.locked{color:var(--del);border-color:rgba(242,99,126,.35);background:rgba(242,99,126,.1)}
/* search results / resolve list share the srch layout */
.srch-list{flex:1;overflow-y:auto;padding:6px 10px 12px}
.srch-row{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:9px;transition:background .12s}
.srch-row:hover{background:var(--hover)}
.srch-body{flex:1;display:flex;flex-direction:column;gap:1px;min-width:0}
.srch-body .n{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.srch-body .p{font-size:11px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.srch-act{flex:0 0 auto;width:30px;height:28px;display:grid;place-items:center;border:1px solid var(--border);
background:var(--panel);border-radius:8px;cursor:pointer;color:var(--muted);font-size:13px}
.srch-act:hover{color:var(--txt);border-color:var(--accent)}.srch-act svg{width:15px;height:15px}
.rslv-bar{display:flex;gap:8px;padding:10px 16px;border-bottom:1px solid var(--border-soft);flex-wrap:wrap}
.rslv-act{flex:0 0 auto;border:1px solid var(--border);background:var(--panel);border-radius:7px;padding:5px 11px;
font-size:11.5px;font-weight:600;color:var(--muted);cursor:pointer;font-family:var(--font)}
.rslv-act:hover{color:var(--txt);border-color:var(--accent)}
/* blame + diff panes */
.picker.blame{width:900px;height:640px}
.blame-body,.diff-body{flex:1;overflow:auto;padding:8px 0;font-family:var(--mono);font-size:12px;line-height:1.5;background:var(--stage-bg)}
.blame-row{display:flex;gap:0;padding:0 12px;white-space:pre}
.blame-row:hover{background:var(--hover)}
.bl-cl{flex:0 0 64px;color:var(--accent-2);font-variant-numeric:tabular-nums}
.bl-user{flex:0 0 110px;color:var(--edit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bl-code{flex:1;color:var(--txt);white-space:pre}
.diff-line{padding:0 14px;white-space:pre;color:var(--muted)}
.diff-line.add{background:rgba(62,207,142,.12);color:var(--diff-add-c)}
.diff-line.del{background:rgba(242,99,126,.12);color:var(--diff-del-c)}
.diff-line.hunk{background:rgba(124,110,246,.1);color:var(--accent-2)}
/* AI settings block inside Settings modal */
.ai-set{margin-top:16px;padding-top:14px;border-top:1px solid var(--border-soft)}