codex-lens 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,4 +29,4 @@
29
29
  * The original design remains. The terminal itself
30
30
  * has been extended to include xterm CSI codes, among
31
31
  * other features.
32
- */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}:root{--bg-primary: #000000;--bg-secondary: #0A0A0A;--bg-tertiary: #141414;--bg-elevated: #1A1A1A;--bg-surface: #0F0F0F;--text-primary: #F8FAFC;--text-secondary: #94A3B8;--text-muted: #64748B;--border-color: rgba(148, 163, 184, .15);--border-glow: rgba(34, 197, 94, .3);--accent-color: #22C55E;--accent-hover: #16A34A;--accent-glow: rgba(34, 197, 94, .4);--accent-soft: rgba(34, 197, 94, .1);--diff-add-bg: rgba(34, 197, 94, .12);--diff-add-text: #4ADE80;--diff-add-border: #22C55E;--diff-remove-bg: rgba(239, 68, 68, .12);--diff-remove-text: #F87171;--diff-remove-border: #EF4444;--danger-color: #EF4444;--danger-hover: #DC2626;--success-color: #22C55E;--success-hover: #16A34A;--warning-color: #F59E0B;--info-color: #3B82F6;--font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", "Consolas", monospace;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .2);--shadow-sm: 0 2px 4px rgba(0, 0, 0, .2);--shadow-md: 0 4px 12px rgba(0, 0, 0, .3);--shadow-lg: 0 8px 24px rgba(0, 0, 0, .4);--shadow-xl: 0 16px 48px rgba(0, 0, 0, .5);--shadow-glow: 0 0 24px var(--accent-glow);--shadow-glow-sm: 0 0 12px var(--accent-glow);--radius-xs: 4px;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 14px;--radius-xl: 20px;--transition-fast: .12s ease;--transition-normal: .2s ease;--transition-slow: .3s ease;--blur-sm: 8px;--blur-md: 16px;--blur-lg: 24px}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}*{margin:0;padding:0;box-sizing:border-box}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-family);background:var(--bg-primary);color:var(--text-primary);font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.app-container{display:flex;flex-direction:column;height:100vh;width:100vw;background:var(--bg-primary)}.top-bar{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:linear-gradient(180deg,#1e293bf2,#1e293bd9);-webkit-backdrop-filter:blur(var(--blur-md));backdrop-filter:blur(var(--blur-md));border-bottom:1px solid var(--border-color);min-height:52px;position:relative;z-index:100}.top-bar:after{content:"";position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--accent-color),transparent);opacity:.3}.top-bar-left,.top-bar-center,.top-bar-right{display:flex;align-items:center;gap:16px}.top-bar-left{width:260px}.top-bar-center{flex:1;justify-content:center}.top-bar-right{width:40%;justify-content:flex-end}.top-bar-title{font-family:var(--font-display);font-weight:600;font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:flex;align-items:center;gap:8px}.top-bar-title:before{content:"";display:inline-block;width:3px;height:14px;background:var(--accent-color);border-radius:2px}.main-content{display:flex;flex:1;overflow:hidden;gap:1px;background:var(--border-color)}.panel{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(180deg,var(--bg-secondary) 0%,rgba(30,41,59,.95) 100%)}.left-panel{width:260px;flex-shrink:0}.middle-panel{flex:1;min-width:300px}.right-panel{width:40%;min-width:300px;max-width:60%}.panel-content{flex:1;overflow:auto;padding:8px}.file-tree{padding:4px}.file-item{padding:8px 12px;cursor:pointer;border-radius:var(--radius-sm);display:flex;align-items:center;gap:10px;transition:all var(--transition-fast);position:relative;margin:2px 0}.file-item:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:0;background:var(--accent-color);border-radius:0 2px 2px 0;transition:height var(--transition-fast)}.file-item:hover{background:#94a3b814}.file-item:hover:before{height:60%}.file-item:active{transform:scale(.98)}.file-item.active{background:var(--accent-soft);color:var(--text-primary)}.file-item.active:before{height:70%}.file-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-size:14px;opacity:.9}.file-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.diff-line{font-family:var(--font-mono);font-size:13px;padding:3px 16px 3px 20px;white-space:pre;border-left:3px solid transparent;transition:background var(--transition-fast);position:relative}.diff-line.added{background:var(--diff-add-bg);color:var(--diff-add-text);border-left-color:var(--diff-add-border)}.diff-line.removed{background:var(--diff-remove-bg);color:var(--diff-remove-text);border-left-color:var(--diff-remove-border)}.diff-line:before{display:inline-block;width:16px;margin-left:-16px;text-align:center;font-weight:600;font-size:12px}.diff-line.added:before{content:"+";color:var(--diff-add-text)}.diff-line.removed:before{content:"-";color:var(--diff-remove-text)}.tab-bar{display:flex;background:#0f172a99;border-bottom:1px solid var(--border-color);overflow-x:auto;min-height:40px;scrollbar-width:none}.tab-bar::-webkit-scrollbar{display:none}.tab{display:flex;align-items:center;gap:8px;padding:10px 16px;background:transparent;border-right:1px solid var(--border-color);cursor:pointer;min-width:100px;max-width:180px;font-size:13px;color:var(--text-muted);transition:all var(--transition-fast);position:relative}.tab:hover{background:#94a3b80d;color:var(--text-secondary)}.tab.active{background:var(--bg-secondary);color:var(--text-primary)}.tab.active:after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background:var(--accent-color)}.tab-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{background:none;border:none;color:var(--text-muted);font-size:16px;cursor:pointer;padding:2px 6px;border-radius:var(--radius-xs);line-height:1;transition:all var(--transition-fast);opacity:.6}.tab-close:hover{background:#ef444433;color:var(--danger-color);opacity:1}.tab.modified .tab-name{color:var(--accent-color)}.tab-modified-mark{color:var(--accent-color);margin-right:4px;font-size:10px}.context-menu{position:fixed;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:6px;min-width:160px;box-shadow:var(--shadow-lg);z-index:1000;animation:contextMenuIn .15s ease;-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}@keyframes contextMenuIn{0%{opacity:0;transform:scale(.95) translateY(-4px)}to{opacity:1;transform:scale(1) translateY(0)}}.context-menu-item{padding:10px 14px;cursor:pointer;font-size:13px;color:var(--text-primary);border-radius:var(--radius-sm);transition:all var(--transition-fast)}.context-menu-item:hover{background:var(--accent-soft);color:var(--accent-color)}.file-context-menu{position:fixed;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:6px;min-width:200px;box-shadow:var(--shadow-lg);z-index:1000;animation:contextMenuIn .15s ease;-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}.ws-status{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:8px;transition:all var(--transition-normal)}.ws-status.connected{background:var(--success-color);box-shadow:0 0 8px var(--success-color);animation:pulse 2s infinite}.ws-status.disconnected{background:var(--danger-color);box-shadow:0 0 8px var(--danger-color)}@keyframes pulse{0%,to{opacity:1;box-shadow:0 0 8px var(--success-color)}50%{opacity:.7;box-shadow:0 0 4px var(--success-color)}}.version-info{display:flex;align-items:center;gap:10px}.version-number{font-size:11px;color:var(--text-muted);font-weight:500;font-family:var(--font-mono);padding:3px 8px;background:#94a3b81a;border-radius:var(--radius-xs)}.update-badge{font-size:10px;padding:4px 10px;background:linear-gradient(135deg,var(--warning-color) 0%,#FBBF24 100%);color:var(--bg-primary);border-radius:var(--radius-sm);cursor:pointer;font-weight:600;transition:all var(--transition-fast)}.update-badge:hover{transform:translateY(-1px);box-shadow:0 4px 12px #f59e0b4d}.terminal-wrapper{flex:1;overflow:hidden;min-height:0}.section{margin-bottom:8px}.section-title{padding:10px 12px 6px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.8px}.empty-state{padding:40px 24px;text-align:center;color:var(--text-muted);font-size:13px;display:flex;flex-direction:column;align-items:center;gap:12px}.empty-state:before{content:"";width:48px;height:48px;background:linear-gradient(135deg,var(--bg-tertiary) 0%,var(--bg-elevated) 100%);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.code-panel{font-family:var(--font-mono)}.code-viewer-codemirror{height:100%;width:100%;display:flex;min-height:0;min-width:0}.code-viewer-codemirror .cm-theme{flex:1;display:flex;min-height:0;min-width:0}.code-viewer-codemirror .cm-editor{flex:1;min-width:0;min-height:0}.code-viewer-codemirror .cm-scroller{flex:1;overflow:auto!important;min-height:0}.code-content{padding:16px;white-space:pre-wrap;word-break:break-all;line-height:1.7;font-size:13px}.diff-container{font-family:var(--font-mono);font-size:13px;padding:8px 0}.task-btn{padding:8px 18px;border:none;border-radius:var(--radius-sm);font-size:12px;font-weight:600;cursor:pointer;transition:all var(--transition-fast);font-family:var(--font-family);display:inline-flex;align-items:center;gap:6px}.task-btn:hover{transform:translateY(-1px)}.task-btn:active{transform:translateY(0)}.task-btn-clear{background:transparent;color:var(--text-secondary);border:1px solid var(--border-color)}.task-btn-clear:hover{background:var(--accent-soft);border-color:var(--accent-color);color:var(--accent-color);box-shadow:var(--shadow-glow-sm)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-elevated);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}::-webkit-scrollbar-corner{background:transparent}@media (max-width: 1024px){.left-panel{width:220px}.right-panel{width:45%;min-width:250px}.top-bar-left{width:220px}}@media (max-width: 768px){.main-content{flex-direction:column}.left-panel,.right-panel{width:100%;max-width:none;min-width:auto}.middle-panel{min-height:300px}.top-bar{flex-wrap:wrap;padding:10px 16px}.top-bar-left,.top-bar-center,.top-bar-right{width:auto}.top-bar-center{order:3;width:100%;justify-content:flex-start;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}}
32
+ */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}:root{--bg-primary: #000000;--bg-secondary: #0A0A0A;--bg-tertiary: #141414;--bg-elevated: #1A1A1A;--bg-surface: #0F0F0F;--text-primary: #F8FAFC;--text-secondary: #94A3B8;--text-muted: #64748B;--border-color: rgba(148, 163, 184, .15);--border-glow: rgba(34, 197, 94, .3);--accent-color: #22C55E;--accent-hover: #16A34A;--accent-glow: rgba(34, 197, 94, .4);--accent-soft: rgba(34, 197, 94, .1);--diff-add-bg: rgba(34, 197, 94, .12);--diff-add-text: #4ADE80;--diff-add-border: #22C55E;--diff-remove-bg: rgba(239, 68, 68, .12);--diff-remove-text: #F87171;--diff-remove-border: #EF4444;--danger-color: #EF4444;--danger-hover: #DC2626;--success-color: #22C55E;--success-hover: #16A34A;--warning-color: #F59E0B;--info-color: #3B82F6;--font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", "Consolas", monospace;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .2);--shadow-sm: 0 2px 4px rgba(0, 0, 0, .2);--shadow-md: 0 4px 12px rgba(0, 0, 0, .3);--shadow-lg: 0 8px 24px rgba(0, 0, 0, .4);--shadow-xl: 0 16px 48px rgba(0, 0, 0, .5);--shadow-glow: 0 0 24px var(--accent-glow);--shadow-glow-sm: 0 0 12px var(--accent-glow);--radius-xs: 4px;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 14px;--radius-xl: 20px;--transition-fast: .12s ease;--transition-normal: .2s ease;--transition-slow: .3s ease;--blur-sm: 8px;--blur-md: 16px;--blur-lg: 24px}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}*{margin:0;padding:0;box-sizing:border-box}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-family);background:var(--bg-primary);color:var(--text-primary);font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.app-container{display:flex;flex-direction:column;height:100vh;width:100vw;background:var(--bg-primary)}.top-bar{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:linear-gradient(180deg,#1e293bf2,#1e293bd9);-webkit-backdrop-filter:blur(var(--blur-md));backdrop-filter:blur(var(--blur-md));border-bottom:1px solid var(--border-color);min-height:52px;position:relative;z-index:100}.top-bar:after{content:"";position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--accent-color),transparent);opacity:.3}.top-bar-left,.top-bar-center,.top-bar-right{display:flex;align-items:center;gap:16px}.top-bar-left{width:260px}.top-bar-center{flex:1;justify-content:center}.top-bar-right{width:40%;justify-content:flex-end}.top-bar-title{font-family:var(--font-display);font-weight:600;font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:flex;align-items:center;gap:8px}.top-bar-title:before{content:"";display:inline-block;width:3px;height:14px;background:var(--accent-color);border-radius:2px}.main-content{display:flex;flex:1;overflow:hidden;gap:1px;background:var(--border-color)}.panel{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(180deg,var(--bg-secondary) 0%,rgba(30,41,59,.95) 100%)}.left-panel{width:260px;flex-shrink:0}.middle-panel{flex:1;min-width:300px}.right-panel{width:40%;min-width:300px;max-width:60%}.panel-content{flex:1;overflow:auto;padding:8px}.file-tree{padding:4px}.file-item{padding:8px 12px;cursor:pointer;border-radius:var(--radius-sm);display:flex;align-items:center;gap:10px;transition:all var(--transition-fast);position:relative;margin:2px 0}.file-item:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:0;background:var(--accent-color);border-radius:0 2px 2px 0;transition:height var(--transition-fast)}.file-item:hover{background:#94a3b814}.file-item:hover:before{height:60%}.file-item:active{transform:scale(.98)}.file-item.active{background:var(--accent-soft);color:var(--text-primary)}.file-item.active:before{height:70%}.file-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-size:14px;opacity:.9}.file-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.diff-line{font-family:var(--font-mono);font-size:13px;padding:3px 16px 3px 20px;white-space:pre;border-left:3px solid transparent;transition:background var(--transition-fast);position:relative}.diff-line.added{background:var(--diff-add-bg);color:var(--diff-add-text);border-left-color:var(--diff-add-border)}.diff-line.removed{background:var(--diff-remove-bg);color:var(--diff-remove-text);border-left-color:var(--diff-remove-border)}.diff-line:before{display:inline-block;width:16px;margin-left:-16px;text-align:center;font-weight:600;font-size:12px}.diff-line.added:before{content:"+";color:var(--diff-add-text)}.diff-line.removed:before{content:"-";color:var(--diff-remove-text)}.tab-bar{display:flex;background:#0f172a99;border-bottom:1px solid var(--border-color);overflow-x:auto;min-height:40px;scrollbar-width:none}.tab-bar::-webkit-scrollbar{display:none}.tab{display:flex;align-items:center;gap:8px;padding:10px 16px;background:transparent;border-right:1px solid var(--border-color);cursor:pointer;min-width:100px;max-width:180px;font-size:13px;color:var(--text-muted);transition:all var(--transition-fast);position:relative}.tab:hover{background:#94a3b80d;color:var(--text-secondary)}.tab.active{background:var(--bg-secondary);color:var(--text-primary)}.tab.active:after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background:var(--accent-color)}.tab-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{background:none;border:none;color:var(--text-muted);font-size:16px;cursor:pointer;padding:2px 6px;border-radius:var(--radius-xs);line-height:1;transition:all var(--transition-fast);opacity:.6}.tab-close:hover{background:#ef444433;color:var(--danger-color);opacity:1}.tab.modified .tab-name{color:var(--accent-color)}.tab-modified-mark{color:var(--accent-color);margin-right:4px;font-size:10px}.context-menu{position:fixed;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:6px;min-width:160px;box-shadow:var(--shadow-lg);z-index:1000;animation:contextMenuIn .15s ease;-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}@keyframes contextMenuIn{0%{opacity:0;transform:scale(.95) translateY(-4px)}to{opacity:1;transform:scale(1) translateY(0)}}.context-menu-item{padding:10px 14px;cursor:pointer;font-size:13px;color:var(--text-primary);border-radius:var(--radius-sm);transition:all var(--transition-fast)}.context-menu-item:hover{background:var(--accent-soft);color:var(--accent-color)}.file-context-menu{position:fixed;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:6px;min-width:200px;box-shadow:var(--shadow-lg);z-index:1000;animation:contextMenuIn .15s ease;-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}.ws-status{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:8px;transition:all var(--transition-normal)}.ws-status.connected{background:var(--success-color);box-shadow:0 0 8px var(--success-color);animation:pulse 2s infinite}.ws-status.disconnected{background:var(--danger-color);box-shadow:0 0 8px var(--danger-color)}@keyframes pulse{0%,to{opacity:1;box-shadow:0 0 8px var(--success-color)}50%{opacity:.7;box-shadow:0 0 4px var(--success-color)}}.version-info{display:flex;align-items:center;gap:10px}.version-number{font-size:11px;color:var(--text-muted);font-weight:500;font-family:var(--font-mono);padding:3px 8px;background:#94a3b81a;border-radius:var(--radius-xs)}.update-badge{font-size:10px;padding:4px 10px;background:linear-gradient(135deg,var(--warning-color) 0%,#FBBF24 100%);color:var(--bg-primary);border-radius:var(--radius-sm);cursor:pointer;font-weight:600;transition:all var(--transition-fast)}.update-badge:hover{transform:translateY(-1px);box-shadow:0 4px 12px #f59e0b4d}.terminal-wrapper{flex:1;overflow:hidden;min-height:0}.section{margin-bottom:8px}.section-title{padding:10px 12px 6px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.8px}.empty-state{padding:40px 24px;text-align:center;color:var(--text-muted);font-size:13px;display:flex;flex-direction:column;align-items:center;gap:12px}.empty-state:before{content:"";width:48px;height:48px;background:linear-gradient(135deg,var(--bg-tertiary) 0%,var(--bg-elevated) 100%);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.code-panel{font-family:var(--font-mono)}.code-viewer-codemirror{height:100%;width:100%;display:flex;min-height:0;min-width:0}.code-viewer-codemirror .cm-theme{flex:1;display:flex;min-height:0;min-width:0}.code-viewer-codemirror .cm-editor{flex:1;min-width:0;min-height:0}.code-viewer-codemirror .cm-scroller{flex:1;overflow:auto!important;min-height:0}.code-content{padding:16px;white-space:pre-wrap;word-break:break-all;line-height:1.7;font-size:13px}.diff-container{font-family:var(--font-mono);font-size:13px;padding:8px 0}.task-btn{padding:8px 18px;border:none;border-radius:var(--radius-sm);font-size:12px;font-weight:600;cursor:pointer;transition:all var(--transition-fast);font-family:var(--font-family);display:inline-flex;align-items:center;gap:6px}.task-btn:hover{transform:translateY(-1px)}.task-btn:active{transform:translateY(0)}.task-btn-clear{background:transparent;color:var(--text-secondary);border:1px solid var(--border-color)}.task-btn-clear:hover{background:var(--accent-soft);border-color:var(--accent-color);color:var(--accent-color);box-shadow:var(--shadow-glow-sm)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-elevated);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}::-webkit-scrollbar-corner{background:transparent}.git-status-bar{display:flex;align-items:center;gap:16px;padding:6px 14px;background:#94a3b814;border-radius:var(--radius-sm);border:1px solid var(--border-color)}.git-branch{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--accent-color)}.git-branch-icon{font-size:14px;opacity:.8}.git-stats{display:flex;align-items:center;gap:12px}.git-staged,.git-unstaged{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary)}.git-staged-count{font-weight:600;color:var(--success-color)}.git-unstaged-count{font-weight:600;color:var(--warning-color)}.git-changes-btn{padding:4px 10px;font-size:11px;background:transparent;border:1px solid var(--border-color);border-radius:var(--radius-xs);color:var(--text-secondary);cursor:pointer;transition:all var(--transition-fast)}.git-changes-btn:hover{background:var(--accent-soft);border-color:var(--accent-color);color:var(--accent-color)}.staged-changes-panel{position:absolute;top:52px;left:0;right:0;max-height:400px;background:linear-gradient(180deg,var(--bg-tertiary) 0%,var(--bg-secondary) 100%);border-bottom:1px solid var(--border-color);z-index:99;overflow-y:auto;animation:slideDown .2s ease}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.staged-panel-header{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:#0f172a99;border-bottom:1px solid var(--border-color);font-weight:600;font-size:12px;color:var(--text-secondary)}.staged-panel-actions{display:flex;gap:8px;align-items:center}.stage-btn,.unstage-btn{padding:4px 10px;font-size:11px;background:var(--bg-elevated);border:1px solid var(--border-color);border-radius:var(--radius-xs);color:var(--text-secondary);cursor:pointer;transition:all var(--transition-fast)}.stage-btn:hover,.unstage-btn:hover{background:var(--accent-soft);border-color:var(--accent-color);color:var(--accent-color)}.close-btn{width:24px;height:24px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;border-radius:var(--radius-xs);transition:all var(--transition-fast)}.close-btn:hover{background:#ef444433;color:var(--danger-color)}.changes-section{padding:8px 0;border-bottom:1px solid var(--border-color)}.changes-section:last-child{border-bottom:none}.changes-section-header{padding:4px 16px}.changes-section-title{font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.changes-list{display:flex;flex-direction:column}.change-item{display:flex;align-items:center;gap:8px;padding:6px 16px;cursor:pointer;transition:background var(--transition-fast)}.change-item:hover{background:#94a3b814}.change-item.staged{background:var(--accent-soft)}.change-icon{width:16px;text-align:center;font-size:12px;color:var(--text-muted)}.change-item.staged .change-icon{color:var(--success-color)}.change-status{width:14px;height:14px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;font-family:var(--font-mono);border-radius:2px}.change-status.M{background:var(--warning-color);color:var(--bg-primary)}.change-status.A{background:var(--success-color);color:var(--bg-primary)}.change-status.D{background:var(--danger-color);color:var(--text-primary)}.change-status.R{background:var(--info-color);color:var(--text-primary)}.change-status.\?{background:var(--text-muted);color:var(--bg-primary)}.change-path{flex:1;font-size:12px;font-family:var(--font-mono);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.no-changes{padding:24px;text-align:center;color:var(--text-muted);font-size:13px}.commit-section{display:flex;gap:8px;padding:12px 16px;background:#0f172a99;border-top:1px solid var(--border-color)}.commit-input{flex:1;padding:8px 12px;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-primary);font-size:12px;font-family:var(--font-family)}.commit-input:focus{outline:none;border-color:var(--accent-color)}.commit-btn{padding:8px 16px;background:var(--accent-color);border:none;border-radius:var(--radius-sm);color:var(--bg-primary);font-size:12px;font-weight:600;cursor:pointer;transition:all var(--transition-fast)}.commit-btn:hover{background:var(--accent-hover);transform:translateY(-1px)}.file-git-badge{width:14px;height:14px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;font-family:var(--font-mono);border-radius:2px;margin-right:2px}.file-git-badge.staged{background:var(--success-color);color:var(--bg-primary)}.file-git-badge.wt{background:var(--warning-color);color:var(--bg-primary)}@media (max-width: 1024px){.left-panel{width:220px}.right-panel{width:45%;min-width:250px}.top-bar-left{width:220px}}@media (max-width: 768px){.main-content{flex-direction:column}.left-panel,.right-panel{width:100%;max-width:none;min-width:auto}.middle-panel{min-height:300px}.top-bar{flex-wrap:wrap;padding:10px 16px}.top-bar-left,.top-bar-center,.top-bar-right{width:auto}.top-bar-center{order:3;width:100%;justify-content:flex-start;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Codex Lens</title>
7
- <script type="module" crossorigin src="./assets/main-BRIK-RhC.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/main-DNXrKVO-.css">
7
+ <script type="module" crossorigin src="./assets/main-8FK9vFAz.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/main-CnRUPtz5.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/dist/watcher.js CHANGED
@@ -109,6 +109,21 @@ class FileWatcher {
109
109
  this.wsEmitter = wsEmitter;
110
110
  this.watcher = null;
111
111
  this.fileContents = /* @__PURE__ */ new Map();
112
+ this.gitStatusCallback = null;
113
+ this._gitStatusTimeout = null;
114
+ }
115
+ setGitStatusCallback(callback) {
116
+ this.gitStatusCallback = callback;
117
+ }
118
+ _scheduleGitStatusUpdate(filePath) {
119
+ if (this.gitStatusCallback) {
120
+ if (this._gitStatusTimeout) {
121
+ clearTimeout(this._gitStatusTimeout);
122
+ }
123
+ this._gitStatusTimeout = setTimeout(() => {
124
+ this.gitStatusCallback(filePath);
125
+ }, 500);
126
+ }
112
127
  }
113
128
  async start() {
114
129
  const ignored = [
@@ -174,6 +189,7 @@ class FileWatcher {
174
189
  }
175
190
  this.emitFileChange(filePath, newContent, diff);
176
191
  }
192
+ this._scheduleGitStatusUpdate(filePath);
177
193
  } catch (error) {
178
194
  logger.errorWithStack(`Error handling file change: ${filePath}`, error);
179
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-lens",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "A visualization tool for Codex that monitors API requests and file system changes",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/aggregator.js CHANGED
@@ -6,6 +6,7 @@ import { createProxyServer } from './proxy.js';
6
6
  import { FileWatcher, scanDirectory } from './watcher.js';
7
7
  import { createLogger } from './lib/logger.js';
8
8
  import { spawnCodex, writeToPty, resizePty, killPty, onPtyData, onPtyExit, getPtyState, getOutputBuffer } from './pty-manager.js';
9
+ import { createGitManager } from './git-manager.js';
9
10
  import path from 'path';
10
11
  import { fileURLToPath } from 'url';
11
12
  import { readFileSync, existsSync, writeFileSync } from 'fs';
@@ -55,6 +56,7 @@ class Aggregator {
55
56
  this.proxyServer = null;
56
57
  this.fileWatcher = null;
57
58
  this.ptyProcess = null;
59
+ this.gitManager = null;
58
60
  }
59
61
 
60
62
  async start(proxyPort) {
@@ -157,6 +159,19 @@ class Aggregator {
157
159
  this.fileWatcher = new FileWatcher(this.projectRoot, (event) => this.broadcast(event));
158
160
  await this.fileWatcher.start();
159
161
 
162
+ // Initialize Git Manager
163
+ this.gitManager = createGitManager(this.projectRoot, (event) => this.broadcast(event));
164
+ if (this.gitManager.isGitRepo()) {
165
+ await this.gitManager.broadcastUpdate();
166
+ logger.info('Git repository detected, git status broadcasting enabled');
167
+ // Set up git status update trigger on file changes
168
+ this.fileWatcher.setGitStatusCallback((filePath) => {
169
+ if (this.gitManager?.isGitRepo()) {
170
+ this.gitManager.scheduleStatusUpdate();
171
+ }
172
+ });
173
+ }
174
+
160
175
  await this.startCodex(proxyPort);
161
176
 
162
177
  return { httpPort: HTTP_PORT, proxyPort };
@@ -248,7 +263,7 @@ class Aggregator {
248
263
  }
249
264
  }
250
265
 
251
- handleClientMessage(data, ws) {
266
+ async handleClientMessage(data, ws) {
252
267
  if (data.type === 'user_message') {
253
268
  writeToPty(data.data + '\r');
254
269
  } else if (data.type === 'open_file') {
@@ -266,6 +281,33 @@ class Aggregator {
266
281
  }
267
282
  }));
268
283
  }
284
+ } else if (data.type === 'git_status_request') {
285
+ if (this.gitManager?.isGitRepo()) {
286
+ this.gitManager.broadcastUpdate();
287
+ }
288
+ } else if (data.type === 'git_stage') {
289
+ if (this.gitManager?.isGitRepo()) {
290
+ if (data.filePath) {
291
+ await this.gitManager.stageFile(data.filePath);
292
+ } else {
293
+ await this.gitManager.stageAll();
294
+ }
295
+ this.gitManager.broadcastUpdate();
296
+ }
297
+ } else if (data.type === 'git_unstage') {
298
+ if (this.gitManager?.isGitRepo()) {
299
+ if (data.filePath) {
300
+ await this.gitManager.unstageFile(data.filePath);
301
+ } else {
302
+ await this.gitManager.unstageAll();
303
+ }
304
+ this.gitManager.broadcastUpdate();
305
+ }
306
+ } else if (data.type === 'git_commit') {
307
+ if (this.gitManager?.isGitRepo()) {
308
+ await this.gitManager.commit(data.message);
309
+ this.gitManager.broadcastUpdate();
310
+ }
269
311
  }
270
312
  }
271
313
 
@@ -13,6 +13,14 @@ export function App() {
13
13
  const [hasUpdate, setHasUpdate] = useState(false);
14
14
  const [projectName, setProjectName] = useState('');
15
15
  const [saving, setSaving] = useState(false);
16
+ const [gitInfo, setGitInfo] = useState({
17
+ isRepo: false,
18
+ branch: null,
19
+ status: null,
20
+ stagedCount: 0,
21
+ unstagedCount: 0
22
+ });
23
+ const [showStagedPanel, setShowStagedPanel] = useState(false);
16
24
  const wsRef = useRef(null);
17
25
 
18
26
  useEffect(() => {
@@ -122,6 +130,15 @@ export function App() {
122
130
  case 'file_content':
123
131
  openFileInTab(msg.data);
124
132
  break;
133
+ case 'git_status':
134
+ setGitInfo({
135
+ isRepo: msg.data.isRepo,
136
+ branch: msg.data.branch,
137
+ status: msg.data.status,
138
+ stagedCount: msg.data.stagedCount,
139
+ unstagedCount: msg.data.unstagedCount
140
+ });
141
+ break;
125
142
  case 'connected':
126
143
  console.log('Server confirmed connection');
127
144
  break;
@@ -130,6 +147,24 @@ export function App() {
130
147
  }
131
148
  }
132
149
 
150
+ function sendGitCommand(type, payload) {
151
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
152
+ wsRef.current.send(JSON.stringify({ type, ...payload }));
153
+ }
154
+ }
155
+
156
+ function handleStage(path) {
157
+ sendGitCommand('git_stage', { filePath: path });
158
+ }
159
+
160
+ function handleUnstage(path) {
161
+ sendGitCommand('git_unstage', { filePath: path });
162
+ }
163
+
164
+ function handleCommit(message) {
165
+ sendGitCommand('git_commit', { message });
166
+ }
167
+
133
168
  function openFileInTab(data) {
134
169
  const fileName = data.path.split(/[/\\]/).pop();
135
170
 
@@ -380,6 +415,30 @@ export function App() {
380
415
  <span className="top-bar-title">当前工作区: {projectName}</span>
381
416
  </div>
382
417
  <div className="top-bar-center">
418
+ {gitInfo.isRepo && (
419
+ <div className="git-status-bar">
420
+ <span className="git-branch">
421
+ <span className="git-branch-icon">⎇</span>
422
+ {gitInfo.branch || 'main'}
423
+ </span>
424
+ <div className="git-stats">
425
+ <span className="git-staged" title="已暂存">
426
+ <span className="git-staged-count">{gitInfo.stagedCount}</span>
427
+ <span className="git-staged-label">已暂存</span>
428
+ </span>
429
+ <span className="git-unstaged" title="未暂存">
430
+ <span className="git-unstaged-count">{gitInfo.unstagedCount}</span>
431
+ <span className="git-unstaged-label">未暂存</span>
432
+ </span>
433
+ </div>
434
+ <button
435
+ className="git-changes-btn"
436
+ onClick={() => setShowStagedPanel(!showStagedPanel)}
437
+ >
438
+ {showStagedPanel ? '隐藏变更' : '查看变更'}
439
+ </button>
440
+ </div>
441
+ )}
383
442
  <button className="task-btn task-btn-clear" onClick={clearAllDiff} title="清空所有 diff 显示">
384
443
  清空 diff
385
444
  </button>
@@ -402,6 +461,7 @@ export function App() {
402
461
  files={files}
403
462
  activeFile={activeTab?.path || null}
404
463
  onFileClick={handleFileClick}
464
+ gitInfo={gitInfo}
405
465
  />
406
466
  <div className="panel middle-panel">
407
467
  <TabBar
@@ -455,6 +515,15 @@ export function App() {
455
515
  }}
456
516
  />
457
517
  )}
518
+ {showStagedPanel && gitInfo.isRepo && (
519
+ <StagedChangesPanel
520
+ gitInfo={gitInfo}
521
+ onStage={handleStage}
522
+ onUnstage={handleUnstage}
523
+ onCommit={handleCommit}
524
+ onClose={() => setShowStagedPanel(false)}
525
+ />
526
+ )}
458
527
  <div className="panel right-panel">
459
528
  <div className="terminal-wrapper">
460
529
  <TerminalPanel />
@@ -516,7 +585,93 @@ function ContextMenu({ x, y, tab, tabs, saving, onClose, onCloseTab, onCloseOthe
516
585
  );
517
586
  }
518
587
 
519
- function LeftPanel({ files, activeFile, onFileClick }) {
588
+ function StagedChangesPanel({ gitInfo, onStage, onUnstage, onCommit, onClose }) {
589
+ if (!gitInfo.isRepo || !gitInfo.status) return null;
590
+
591
+ const { staged, unstaged, untracked } = gitInfo.status;
592
+
593
+ return (
594
+ <div className="staged-changes-panel">
595
+ <div className="staged-panel-header">
596
+ <span>Git 变更</span>
597
+ <div className="staged-panel-actions">
598
+ <button onClick={() => onStage(null)} className="stage-btn">Stage All</button>
599
+ <button onClick={() => onUnstage(null)} className="unstage-btn">Unstage All</button>
600
+ <button onClick={onClose} className="close-btn">×</button>
601
+ </div>
602
+ </div>
603
+
604
+ {staged?.length > 0 && (
605
+ <div className="changes-section">
606
+ <div className="changes-section-header">
607
+ <span className="changes-section-title">已暂存 ({staged.length})</span>
608
+ </div>
609
+ <div className="changes-list">
610
+ {staged.map((file, i) => (
611
+ <div key={i} className="change-item staged" onClick={() => onUnstage(file.path)}>
612
+ <span className="change-icon">✓</span>
613
+ <span className={`change-status ${file.indexStatus}`}>{file.indexStatus}</span>
614
+ <span className="change-path">{file.path}</span>
615
+ </div>
616
+ ))}
617
+ </div>
618
+ </div>
619
+ )}
620
+
621
+ {unstaged?.length > 0 && (
622
+ <div className="changes-section">
623
+ <div className="changes-section-header">
624
+ <span className="changes-section-title">未暂存修改 ({unstaged.length})</span>
625
+ </div>
626
+ <div className="changes-list">
627
+ {unstaged.map((file, i) => (
628
+ <div key={i} className="change-item" onClick={() => onStage(file.path)}>
629
+ <span className="change-icon">○</span>
630
+ <span className={`change-status ${file.workTreeStatus}`}>{file.workTreeStatus}</span>
631
+ <span className="change-path">{file.path}</span>
632
+ </div>
633
+ ))}
634
+ </div>
635
+ </div>
636
+ )}
637
+
638
+ {untracked?.length > 0 && (
639
+ <div className="changes-section">
640
+ <div className="changes-section-header">
641
+ <span className="changes-section-title">未跟踪 ({untracked.length})</span>
642
+ </div>
643
+ <div className="changes-list">
644
+ {untracked.map((file, i) => (
645
+ <div key={i} className="change-item untracked" onClick={() => onStage(file.path)}>
646
+ <span className="change-icon">?</span>
647
+ <span className="change-status">?</span>
648
+ <span className="change-path">{file.path}</span>
649
+ </div>
650
+ ))}
651
+ </div>
652
+ </div>
653
+ )}
654
+
655
+ {((!staged?.length) && (!unstaged?.length) && (!untracked?.length)) && (
656
+ <div className="no-changes">无变更</div>
657
+ )}
658
+
659
+ {staged?.length > 0 && (
660
+ <div className="commit-section">
661
+ <input type="text" className="commit-input" placeholder="提交信息..." id="commit-message" />
662
+ <button className="commit-btn" onClick={() => {
663
+ const msg = document.getElementById('commit-message')?.value;
664
+ if (msg) onCommit(msg);
665
+ }}>
666
+ Commit
667
+ </button>
668
+ </div>
669
+ )}
670
+ </div>
671
+ );
672
+ }
673
+
674
+ function LeftPanel({ files, activeFile, onFileClick, gitInfo }) {
520
675
  const [expandedDirs, setExpandedDirs] = useState({});
521
676
  const [contextMenu, setContextMenu] = useState(null);
522
677
 
@@ -567,6 +722,14 @@ function LeftPanel({ files, activeFile, onFileClick }) {
567
722
  const isExpanded = expandedDirs[item.path];
568
723
  const indent = 8 + depth * 16;
569
724
 
725
+ // Get file git status
726
+ let fileGitStatus = null;
727
+ if (!isDir && gitInfo.status) {
728
+ fileGitStatus = gitInfo.status.staged?.find(f => f.path === item.path) ||
729
+ gitInfo.status.unstaged?.find(f => f.path === item.path) ||
730
+ gitInfo.status.untracked?.find(f => f.path === item.path);
731
+ }
732
+
570
733
  return (
571
734
  <React.Fragment key={item.path}>
572
735
  <div
@@ -576,6 +739,11 @@ function LeftPanel({ files, activeFile, onFileClick }) {
576
739
  onContextMenu={(e) => handleContextMenu(e, item)}
577
740
  style={{ paddingLeft: `${indent}px` }}
578
741
  >
742
+ {fileGitStatus && (
743
+ <span className={`file-git-badge ${fileGitStatus.indexStatus !== '?' ? 'staged' : 'wt'}`}>
744
+ {fileGitStatus.indexStatus !== '?' ? fileGitStatus.indexStatus : fileGitStatus.workTreeStatus}
745
+ </span>
746
+ )}
579
747
  <span className="file-icon">
580
748
  {isDir ? (isExpanded ? '📂' : '📁') : getFileIcon(item.type)}
581
749
  </span>
@@ -0,0 +1,168 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { createLogger } from './lib/logger.js';
5
+
6
+ const logger = createLogger('GitManager');
7
+
8
+ class GitManager {
9
+ constructor(projectRoot, wsEmitter) {
10
+ this.projectRoot = projectRoot;
11
+ this.wsEmitter = wsEmitter;
12
+ this.gitDir = join(projectRoot, '.git');
13
+ this.currentStatus = null;
14
+ this.currentBranch = null;
15
+ this._statusTimeout = null;
16
+ }
17
+
18
+ isGitRepo() {
19
+ return existsSync(this.gitDir);
20
+ }
21
+
22
+ runGitCommand(args) {
23
+ return new Promise((resolve, reject) => {
24
+ const proc = spawn('git', args, {
25
+ cwd: this.projectRoot,
26
+ shell: true,
27
+ windowsHide: true
28
+ });
29
+
30
+ let stdout = '';
31
+ let stderr = '';
32
+
33
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
34
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
35
+
36
+ proc.on('close', (code) => {
37
+ resolve({ code, stdout, stderr });
38
+ });
39
+
40
+ proc.on('error', (err) => {
41
+ reject(err);
42
+ });
43
+ });
44
+ }
45
+
46
+ parsePorcelainStatus(output) {
47
+ const lines = output.trim().split('\n');
48
+ const result = {
49
+ staged: [],
50
+ unstaged: [],
51
+ untracked: [],
52
+ conflicted: []
53
+ };
54
+
55
+ for (const line of lines) {
56
+ if (!line || line.length < 3) continue;
57
+
58
+ const indexStatus = line[0];
59
+ const workTreeStatus = line[1];
60
+ const path = line.slice(3).trim();
61
+
62
+ const fileInfo = { path, indexStatus, workTreeStatus };
63
+
64
+ // Staged changes (index)
65
+ if (indexStatus !== ' ' && indexStatus !== '?') {
66
+ result.staged.push(fileInfo);
67
+ }
68
+
69
+ // Working tree changes
70
+ if (workTreeStatus === 'M' || workTreeStatus === 'D') {
71
+ result.unstaged.push(fileInfo);
72
+ }
73
+
74
+ // Untracked files
75
+ if (indexStatus === '?' && workTreeStatus === '?') {
76
+ result.untracked.push(fileInfo);
77
+ }
78
+
79
+ // Conflicted files
80
+ if (indexStatus === 'U' || workTreeStatus === 'U') {
81
+ result.conflicted.push(fileInfo);
82
+ }
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ async getStatus() {
89
+ if (!this.isGitRepo()) return null;
90
+
91
+ try {
92
+ const { stdout } = await this.runGitCommand(['status', '--porcelain']);
93
+ this.currentStatus = this.parsePorcelainStatus(stdout);
94
+ return this.currentStatus;
95
+ } catch (error) {
96
+ logger.error(`Failed to get git status: ${error.message}`);
97
+ return null;
98
+ }
99
+ }
100
+
101
+ async getCurrentBranch() {
102
+ if (!this.isGitRepo()) return null;
103
+
104
+ try {
105
+ const { stdout } = await this.runGitCommand(['branch', '--show-current']);
106
+ this.currentBranch = stdout.trim();
107
+ return this.currentBranch;
108
+ } catch (error) {
109
+ logger.error(`Failed to get branch: ${error.message}`);
110
+ return null;
111
+ }
112
+ }
113
+
114
+ async stageFile(filePath) {
115
+ await this.runGitCommand(['add', filePath]);
116
+ return this.getStatus();
117
+ }
118
+
119
+ async unstageFile(filePath) {
120
+ await this.runGitCommand(['reset', 'HEAD', '--', filePath]);
121
+ return this.getStatus();
122
+ }
123
+
124
+ async stageAll() {
125
+ await this.runGitCommand(['add', '-A']);
126
+ return this.getStatus();
127
+ }
128
+
129
+ async unstageAll() {
130
+ await this.runGitCommand(['reset', 'HEAD']);
131
+ return this.getStatus();
132
+ }
133
+
134
+ async commit(message) {
135
+ await this.runGitCommand(['commit', '-m', message]);
136
+ return this.getStatus();
137
+ }
138
+
139
+ async broadcastUpdate() {
140
+ const branch = await this.getCurrentBranch();
141
+ const status = await this.getStatus();
142
+
143
+ this.wsEmitter({
144
+ type: 'git_status',
145
+ data: {
146
+ isRepo: true,
147
+ branch,
148
+ status,
149
+ stagedCount: status?.staged?.length || 0,
150
+ unstagedCount: (status?.unstaged?.length || 0) + (status?.untracked?.length || 0),
151
+ timestamp: new Date().toISOString()
152
+ }
153
+ });
154
+ }
155
+
156
+ scheduleStatusUpdate() {
157
+ if (this._statusTimeout) {
158
+ clearTimeout(this._statusTimeout);
159
+ }
160
+ this._statusTimeout = setTimeout(() => {
161
+ this.broadcastUpdate();
162
+ }, 500);
163
+ }
164
+ }
165
+
166
+ export function createGitManager(projectRoot, wsEmitter) {
167
+ return new GitManager(projectRoot, wsEmitter);
168
+ }