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:
100
src/App.css
100
src/App.css
@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user