diff --git a/docs/agents/repo-instructions.md b/docs/agents/repo-instructions.md index c5365fd..3992a53 100644 --- a/docs/agents/repo-instructions.md +++ b/docs/agents/repo-instructions.md @@ -73,3 +73,13 @@ If this project is used in a completely local-only or air-gapped context, refer - For self-hosted instances, include `--host` in commands unless the git remote already points at the correct instance. - `interrogate` and `to-prd` should read this file to know which CLI and host conventions to follow. - When in doubt about the current instance, check `git remote -v` first. + +## Troubleshooting `fj` Failures + +If a `fj` command fails on this or any self-hosted Forgejo instance, do not retry with different flags. Diagnose first: + +1. `git remote -v` — note the URL form. URL-style remotes with explicit non-default ports (`ssh://git@host:NNNN/...`) cause `fj` to use that same port for HTTPS API calls, producing TLS errors. SCP-style remotes (`git@host:owner/repo.git`) avoid this; route non-default SSH ports via `~/.ssh/config`. +2. `cat ~/.local/share/forgejo-cli/keys.json` — the host key string is what `fj` uses for both auth lookup and API URL. A `:NNNN` suffix here is the most common root cause. +3. `fj whoami` (in-repo) and `fj -H https:// whoami` — comparing these tells you whether auth is stored under the bare host or under host-with-port. + +`fj`'s syntax also has sharp edges worth noting: `fj issue comment [BODY]` takes the body as a positional argument (not `--body`); there is no `fj issue list` — use `fj issue search "" -s open|closed|all`; and cross-repo issue references use `owner/repo#N`. When in doubt, run ` --help` — `fj` is `clap`-based and its help output is reliable. diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 3597e91..016c138 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -12,10 +12,11 @@ PluginComponent { property bool isConnected: false property string tailscaleIP: "" property string currentExitNode: "" + property var peers: [] layerNamespacePlugin: "tailscalectl" popoutWidth: 360 - popoutHeight: 200 + popoutHeight: 400 Timer { interval: 5000 @@ -46,10 +47,19 @@ PluginComponent { root.isConnected = data.BackendState === "Running" root.tailscaleIP = (data.Self?.TailscaleIPs?.[0]) || "" root.currentExitNode = data.CurrentExitNode?.HostName || "" + root.peers = Object.keys(data.Peers || {}).map(function(key) { + var p = data.Peers[key] + return { + hostname: p.HostName || key, + ip: (p.TailscaleIPs && p.TailscaleIPs.length) ? p.TailscaleIPs[0] : "", + online: p.Online || false + } + }) } catch (e) { root.isConnected = false root.tailscaleIP = "" root.currentExitNode = "" + root.peers = [] ToastService.showError("tailscalectl", "Failed to parse Tailscale status") } } @@ -60,6 +70,7 @@ PluginComponent { root.isConnected = false root.tailscaleIP = "" root.currentExitNode = "" + root.peers = [] } } } @@ -73,6 +84,11 @@ PluginComponent { toggleProcess.running = true } + function copyToClipboard(text) { + Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"]) + ToastService.showInfo("tailscalectl", "Copied: " + text) + } + popoutContent: Component { PopoutComponent { headerText: "Tailscale" @@ -80,8 +96,9 @@ PluginComponent { showCloseButton: true Item { + id: contentItem width: parent.width - height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingS + exitNodeText.implicitHeight + Theme.spacingM + height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + peerList.height + Theme.spacingM Row { id: statusRow @@ -128,6 +145,65 @@ PluginComponent { anchors.verticalCenter: parent.verticalCenter } } + + ListView { + id: peerList + y: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + width: parent.width - Theme.spacingM * 2 + height: Math.min(root.peers.length * (Theme.fontSizeSmall + Theme.spacingXS), 200) + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + model: root.peers + interactive: true + boundsBehavior: Flickable.DragAndOvershootBounds + + delegate: Item { + width: peerList.width + height: Theme.fontSizeSmall + Theme.spacingXS + + Row { + anchors.fill: parent + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + anchors.verticalCenter: parent.verticalCenter + width: peerHostnameText.implicitWidth + height: peerHostnameText.implicitHeight + onClicked: { + root.copyToClipboard(modelData.hostname) + } + + StyledText { + id: peerHostnameText + text: modelData.hostname + font.pixelSize: Theme.fontSizeSmall + color: modelData.online ? Theme.primary : Theme.surfaceVariantText + } + } + + MouseArea { + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + anchors.verticalCenter: parent.verticalCenter + width: peerIpText.implicitWidth + height: peerIpText.implicitHeight + onClicked: { + root.copyToClipboard(modelData.ip) + } + + StyledText { + id: peerIpText + text: modelData.ip + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } } } }