From 42456f25d28a7d09809e2e0e4230632fcb09c934 Mon Sep 17 00:00:00 2001 From: Cowork Supervisor Date: Sun, 12 Jul 2026 13:04:49 -0400 Subject: [PATCH] fix: block duplicate env/header keys at entry instead of silently dropping one (#38) Closes #38 Co-Authored-By: Claude Fable 5 --- bcc.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/bcc.py b/bcc.py index dbd6110..a219285 100644 --- a/bcc.py +++ b/bcc.py @@ -19,6 +19,7 @@ from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, S from PySide6.QtGui import ( QAction, QColor, + QCursor, QDesktopServices, QGuiApplication, QIcon, @@ -51,6 +52,7 @@ from PySide6.QtWidgets import ( QStyledItemDelegate, QTableWidget, QTableWidgetItem, + QToolTip, QVBoxLayout, QWidget, ) @@ -270,10 +272,31 @@ class KeyValueTable(QWidget): self.reveal_btn.setText("Hide secrets" if on else "Show secrets") self.table.viewport().update() - def _changed(self, *_): + def _changed(self, item=None, *_): + if item is not None and item.column() == 0: + new_key = item.text().strip() + row = item.row() + if new_key and self._is_duplicate_key(new_key, row): + prev_key = item.data(Qt.ItemDataRole.UserRole) + prev_key = prev_key if prev_key is not None else "" + self.table.blockSignals(True) + item.setText(prev_key) + self.table.blockSignals(False) + QToolTip.showText(QCursor.pos(), f"Duplicate key '{new_key}' — reverted.") + return + item.setData(Qt.ItemDataRole.UserRole, new_key) if self._on_change: self._on_change() + def _is_duplicate_key(self, key: str, ignore_row: int) -> bool: + for r in range(self.table.rowCount()): + if r == ignore_row: + continue + other = self.table.item(r, 0) + if other and other.text().strip() == key: + return True + return False + def _add_row(self): dlg = QDialog(self.window()) dlg.setWindowTitle(f"Add {self._key_label}") @@ -300,6 +323,13 @@ class KeyValueTable(QWidget): grid.addWidget(val_edit, 1, 1) v.addLayout(grid) + # Shown when the typed key already exists in the table. + dup_warn = QLabel("") + dup_warn.setStyleSheet(f"color: {WARN};") + dup_warn.setWordWrap(True) + dup_warn.hide() + v.addWidget(dup_warn) + # Type an API_KEY/TOKEN-style name and the value field masks itself. def _sync_echo(text): secret = core.is_secret_key(text) @@ -315,7 +345,15 @@ class KeyValueTable(QWidget): ok_btn = btns.button(QDialogButtonBox.StandardButton.Ok) ok_btn.setObjectName("primary") ok_btn.setEnabled(False) - key_edit.textChanged.connect(lambda t: ok_btn.setEnabled(bool(t.strip()))) + + def _validate(text): + k = text.strip() + dup = bool(k) and self._is_duplicate_key(k, ignore_row=-1) + ok_btn.setEnabled(bool(k) and not dup) + dup_warn.setText(f"'{k}' already exists" if dup else "") + dup_warn.setVisible(dup) + + key_edit.textChanged.connect(_validate) btns.accepted.connect(dlg.accept) btns.rejected.connect(dlg.reject) v.addWidget(btns) @@ -329,13 +367,15 @@ class KeyValueTable(QWidget): return k = key_edit.text().strip() val = val_edit.text() - if not k: + if not k or self._is_duplicate_key(k, ignore_row=-1): return if self._before_change: self._before_change() r = self.table.rowCount() self.table.insertRow(r) - self.table.setItem(r, 0, QTableWidgetItem(k)) + key_item = QTableWidgetItem(k) + key_item.setData(Qt.ItemDataRole.UserRole, k) + self.table.setItem(r, 0, key_item) self.table.setItem(r, 1, QTableWidgetItem(val)) self._changed() @@ -354,7 +394,9 @@ class KeyValueTable(QWidget): for k, v in (d or {}).items(): r = self.table.rowCount() self.table.insertRow(r) - self.table.setItem(r, 0, QTableWidgetItem(str(k))) + key_item = QTableWidgetItem(str(k)) + key_item.setData(Qt.ItemDataRole.UserRole, str(k)) + self.table.setItem(r, 0, key_item) self.table.setItem(r, 1, QTableWidgetItem(str(v))) self.table.blockSignals(False) -- 2.52.0