bcc.py: unify paste/drop collision handling via shared _import_server() prompt

This commit is contained in:
2026-07-07 20:26:00 -04:00
parent 0f1cdbef3c
commit 2a0802b22f
+53 -34
View File
@@ -374,6 +374,9 @@ class ServerEditor(QFrame):
self.spawn_btn.setToolTip("Spawn the server for 3 s and report whether it starts cleanly") self.spawn_btn.setToolTip("Spawn the server for 3 s and report whether it starts cleanly")
self.spawn_btn.clicked.connect(self._test_spawn) self.spawn_btn.clicked.connect(self._test_spawn)
self.spawn_btn.setVisible(False) self.spawn_btn.setVisible(False)
self.logs_btn = QPushButton("View logs")
self.logs_btn.setToolTip("Open this server's MCP log in a read-only, auto-tailing viewer")
self.logs_btn.clicked.connect(self._view_logs)
self.details_btn = QPushButton("Details ▸") self.details_btn = QPushButton("Details ▸")
self.details_btn.setCheckable(True) self.details_btn.setCheckable(True)
self.details_btn.toggled.connect(self._toggle_diag) self.details_btn.toggled.connect(self._toggle_diag)
@@ -384,6 +387,7 @@ class ServerEditor(QFrame):
dep.addWidget(self.fix_btn) dep.addWidget(self.fix_btn)
dep.addWidget(self.test_btn) dep.addWidget(self.test_btn)
dep.addWidget(self.spawn_btn) dep.addWidget(self.spawn_btn)
dep.addWidget(self.logs_btn)
dep.addWidget(self.details_btn) dep.addWidget(self.details_btn)
dep.addWidget(recheck) dep.addWidget(recheck)
outer.addLayout(dep) outer.addLayout(dep)
@@ -728,6 +732,13 @@ class ServerEditor(QFrame):
elif self.diag_card.isVisible(): elif self.diag_card.isVisible():
self.diag_text.setPlainText(self._full_diag_text()) self.diag_text.setPlainText(self._full_diag_text())
def _view_logs(self):
name = self.current_name()
if not name:
return
dlg = LogViewerDialog(self.window(), name)
dlg.exec()
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Arguments editor: one line = one argument, with a numbered gutter so that # Arguments editor: one line = one argument, with a numbered gutter so that
@@ -1104,7 +1115,7 @@ class MainWindow(QMainWindow):
self.full_config: dict = {} self.full_config: dict = {}
self.servers: list[core.ServerEntry] = [] self.servers: list[core.ServerEntry] = []
self.current_profile: core.Profile | None = None self.current_profile: core.Profile | None = None
self._loaded_mtime: float | None = None self._loaded_stat: core.ConfigStat | None = None
self.dirty = False self.dirty = False
self._suppress_table = False self._suppress_table = False
self._suppress_sel = False self._suppress_sel = False
@@ -1380,7 +1391,7 @@ class MainWindow(QMainWindow):
return return
self.full_config = cfg self.full_config = cfg
repaired = True repaired = True
self._loaded_mtime = core.config_mtime(profile.path) self._loaded_stat = core.config_fingerprint(profile.path)
self.current_profile = profile self.current_profile = profile
self.servers = core.extract_servers(self.full_config) self.servers = core.extract_servers(self.full_config)
self.dirty = False self.dirty = False
@@ -1620,34 +1631,38 @@ class MainWindow(QMainWindow):
self._refresh_tables(select_index=min(idx, len(self.servers) - 1)) self._refresh_tables(select_index=min(idx, len(self.servers) - 1))
self._mark_dirty() self._mark_dirty()
def _import_server(self, name: str, data: dict) -> tuple[bool, bool]:
"""
Add a pasted/dropped `name`/`data` server to self.servers, resolving
a name collision with an explicit prompt (never a silent overwrite).
Returns (added, replaced).
"""
existing = {s.name: i for i, s in enumerate(self.servers)}
if name in existing:
ans = QMessageBox.question(
self,
"Server exists",
f"{name}” already exists. Replace it?\n\nYes = replace · No = keep both (renamed)",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if ans == QMessageBox.StandardButton.Yes:
self.servers[existing[name]].data = data
return False, True
name = core.resolve_name_collision(name, {s.name for s in self.servers})
self.servers.append(core.ServerEntry(name, data, True))
return True, False
def paste_json(self): def paste_json(self):
dlg = PasteDialog(self) dlg = PasteDialog(self)
if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.result_servers: if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.result_servers:
return return
self._push_undo() self._push_undo()
added, replaced = 0, 0 added, replaced = 0, 0
existing = {s.name: i for i, s in enumerate(self.servers)}
for name, data in dlg.result_servers.items(): for name, data in dlg.result_servers.items():
if name in existing: a, r = self._import_server(name, data)
ans = QMessageBox.question( added += int(a)
self, replaced += int(r)
"Server exists",
f"{name}” already exists. Replace it?\n\nYes = replace · No = keep both (renamed)",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if ans == QMessageBox.StandardButton.Yes:
self.servers[existing[name]].data = data
replaced += 1
continue
new = f"{name}-2"
k = 3
names = {s.name for s in self.servers}
while new in names:
new = f"{name}-{k}"
k += 1
name = new
self.servers.append(core.ServerEntry(name, data, True))
added += 1
self._refresh_tables(select_index=len(self.servers) - 1) self._refresh_tables(select_index=len(self.servers) - 1)
self._mark_dirty() self._mark_dirty()
self.status.setText(f"Imported {added} added, {replaced} replaced. Review and Save.") self.status.setText(f"Imported {added} added, {replaced} replaced. Review and Save.")
@@ -1719,11 +1734,14 @@ class MainWindow(QMainWindow):
return return
# Stale-file check: if the file changed on disk since we loaded it, prompt. # Stale-file check: if the file changed on disk since we loaded it, prompt.
disk_mtime = core.config_mtime(self.current_profile.path) # Compare mtime AND size (not mtime alone) so a concurrent external write
# that lands within the mtime resolution window, or that restores the
# original mtime, still gets caught.
disk_stat = core.config_fingerprint(self.current_profile.path)
if ( if (
disk_mtime is not None disk_stat is not None
and self._loaded_mtime is not None and self._loaded_stat is not None
and disk_mtime != self._loaded_mtime and disk_stat != self._loaded_stat
): ):
changed_keys, server_diff = core.external_change_summary( changed_keys, server_diff = core.external_change_summary(
self.full_config, self.current_profile.path self.full_config, self.current_profile.path
@@ -1744,7 +1762,7 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self, "Save failed", str(e)) QMessageBox.critical(self, "Save failed", str(e))
return return
self.full_config = fresh self.full_config = fresh
self._loaded_mtime = core.config_mtime(self.current_profile.path) self._loaded_stat = core.config_fingerprint(self.current_profile.path)
self.current_profile.config_exists = True self.current_profile.config_exists = True
self.dirty = False self.dirty = False
self.save_btn.setEnabled(False) self.save_btn.setEnabled(False)
@@ -1762,7 +1780,7 @@ class MainWindow(QMainWindow):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Save failed", str(e)) QMessageBox.critical(self, "Save failed", str(e))
return return
self._loaded_mtime = core.config_mtime(self.current_profile.path) self._loaded_stat = core.config_fingerprint(self.current_profile.path)
self.current_profile.config_exists = True self.current_profile.config_exists = True
self.dirty = False self.dirty = False
self.save_btn.setEnabled(False) self.save_btn.setEnabled(False)
@@ -1835,15 +1853,16 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Couldn't import", f"{Path(path).name}:\n{ex}") QMessageBox.warning(self, "Couldn't import", f"{Path(path).name}:\n{ex}")
continue continue
self._push_undo() self._push_undo()
added, replaced = 0, 0
for name, data in servers.items(): for name, data in servers.items():
names = {s.name for s in self.servers} a, r = self._import_server(name, data)
if name in names: added += int(a)
name = f"{name}-imported" replaced += int(r)
self.servers.append(core.ServerEntry(name, data, True))
self._refresh_tables(select_index=len(self.servers) - 1) self._refresh_tables(select_index=len(self.servers) - 1)
self._mark_dirty() self._mark_dirty()
self.status.setText( self.status.setText(
f"Imported {len(servers)} server(s) from {Path(path).name}. Review and Save." f"Imported {added} added, {replaced} replaced from {Path(path).name}. "
"Review and Save."
) )
break break