feat(#83): offer move-to-env on args rows; explain the gate instead of an empty menu
CI / Lint (ruff) (pull_request) Successful in 19s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 31s
CI / Catalog signature (pull_request) Successful in 22s
CI / Lint (ruff) (pull_request) Successful in 19s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 31s
CI / Catalog signature (pull_request) Successful in 22s
Two things surfaced testing the GUI:
1. Args rows showed the secret warning but no move action -- the args
editor is a free-text widget, not a table, and was deliberately left out
of the first cut. Wired it up: ArgsEdit gains a context menu that offers
"Move to environment variable…" on exactly the args that look like a
credential. New pure core: secret_arg_indices (which args are secrets,
mirroring args_secret_warning per-index) and suggested_env_var_for_arg
(default var name from the preceding flag -- `--api-key <secret>` ->
API_KEY, else SECRET). The move replaces that one arg line with ${VAR}
and copies the secret to the clipboard, same contract as the tables.
2. On a Claude Desktop profile (or any non-expanding client) the menu showed
NOTHING, so it read as broken. Now a real stored secret always shows the
item -- enabled on a client that expands references, or disabled with the
reason ("unavailable for Claude Desktop -- it doesn't expand ${VAR}") so
the gate is visible rather than silent. Applies to env, headers and args.
Not a change: after converting, env_ref_warnings still notes a variable that
isn't set in the environment. That's #82's advisory doing its job -- the user
runs the export line the dialog handed them; auto-adding a ':-default' would
bake a fallback back into the config and defeat moving the secret out.
Tests: +5 core (secret_arg_indices for token/flag-value/embedded-URL/
reference-excluded, suggested_env_var_for_arg with and without a flag).
483 passed, ruff clean. GUI wiring (context menus) remains untestable in CI.
Refs #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
This commit is contained in:
@@ -418,12 +418,23 @@ class KeyValueTable(QWidget):
|
||||
return
|
||||
row = item.row()
|
||||
key, value = self._row_key_value(row)
|
||||
profile = self.profile_provider() if self.profile_provider else None
|
||||
if not core.can_move_value_to_env_ref(key, value, profile):
|
||||
# Only a real stored secret is worth moving; nothing to offer otherwise.
|
||||
if not core.should_mask_value(key, value):
|
||||
return
|
||||
profile = self.profile_provider() if self.profile_provider else None
|
||||
menu = QMenu(self)
|
||||
act = QAction("Move to environment variable…", self)
|
||||
act.triggered.connect(lambda: self._move_row_to_env(row))
|
||||
if core.can_move_value_to_env_ref(key, value, profile):
|
||||
act.triggered.connect(lambda: self._move_row_to_env(row))
|
||||
else:
|
||||
# Show it disabled with the reason rather than an empty menu, so the
|
||||
# feature is discoverable and Claude Desktop's gating is explained.
|
||||
act.setEnabled(False)
|
||||
act.setText("Move to environment variable — unavailable for Claude Desktop")
|
||||
act.setToolTip(
|
||||
"Claude Desktop doesn't expand ${VAR}, so a reference would reach "
|
||||
"the server as literal text."
|
||||
)
|
||||
menu.addAction(act)
|
||||
menu.exec(self.table.viewport().mapToGlobal(pos))
|
||||
|
||||
@@ -762,10 +773,11 @@ class ServerEditor(QFrame):
|
||||
return w
|
||||
|
||||
def set_profile_provider(self, provider):
|
||||
"""Let the env/headers tables gate "move to environment variable" on
|
||||
which client the loaded profile targets (#83)."""
|
||||
"""Let the env/headers tables and the args editor gate "move to
|
||||
environment variable" on which client the loaded profile targets (#83)."""
|
||||
self.env.profile_provider = provider
|
||||
self.headers.profile_provider = provider
|
||||
self.args.profile_provider = provider
|
||||
|
||||
# --- model <-> form -------------------------------------------------- #
|
||||
def load_entry(self, entry: core.ServerEntry | None):
|
||||
@@ -1044,6 +1056,50 @@ class ArgsEdit(QPlainTextEdit):
|
||||
self.blockCountChanged.connect(self._update_gutter_width)
|
||||
self.updateRequest.connect(self._on_update_request)
|
||||
self._update_gutter_width()
|
||||
# Set by ServerEditor so "move to environment variable" (#83) can gate on
|
||||
# the loaded client, same as the env/headers tables.
|
||||
self.profile_provider = None
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
menu = self.createStandardContextMenu() # keep cut/copy/paste
|
||||
lines = self.toPlainText().splitlines()
|
||||
block = self.cursorForPosition(event.pos()).blockNumber()
|
||||
if 0 <= block < len(lines) and lines[block].strip():
|
||||
# This editor is one arg per line; map the clicked block to its
|
||||
# index among the non-blank args the model actually sees.
|
||||
cleaned = [ln for ln in lines if ln.strip() != ""]
|
||||
idx = sum(1 for ln in lines[:block] if ln.strip() != "")
|
||||
if idx in set(core.secret_arg_indices(cleaned)):
|
||||
profile = self.profile_provider() if self.profile_provider else None
|
||||
act = QAction("Move to environment variable…", self)
|
||||
if profile is None or core.client_expands_env_refs(profile):
|
||||
act.triggered.connect(lambda: self._move_arg_to_env(block, idx, cleaned))
|
||||
else:
|
||||
act.setEnabled(False)
|
||||
act.setText("Move to environment variable — unavailable for Claude Desktop")
|
||||
act.setToolTip(
|
||||
"Claude Desktop doesn't expand ${VAR}, so a reference would "
|
||||
"reach the server as literal text."
|
||||
)
|
||||
first = menu.actions()[0] if menu.actions() else None
|
||||
menu.insertAction(first, act)
|
||||
if first is not None:
|
||||
menu.insertSeparator(first)
|
||||
menu.exec(event.globalPos())
|
||||
|
||||
def _move_arg_to_env(self, block: int, idx: int, cleaned: list):
|
||||
value = cleaned[idx]
|
||||
default_name = core.suggested_env_var_for_arg(cleaned, idx)
|
||||
dlg = MoveToEnvDialog(self.window(), default_name, value)
|
||||
if not dlg.exec():
|
||||
return
|
||||
var_name = dlg.var_name()
|
||||
QGuiApplication.clipboard().setText(value)
|
||||
lines = self.toPlainText().splitlines()
|
||||
if 0 <= block < len(lines):
|
||||
lines[block] = f"${{{var_name}}}"
|
||||
# setPlainText fires textChanged -> _emit (dirty + arg recheck).
|
||||
self.setPlainText("\n".join(lines))
|
||||
|
||||
def gutter_width(self) -> int:
|
||||
digits = max(1, len(str(self.blockCount())))
|
||||
|
||||
Reference in New Issue
Block a user