From bbd1f81a81a40816ef9d980192569fdc38f1c799 Mon Sep 17 00:00:00 2001
From: aksops
Date: Fri, 1 May 2026 10:28:13 +0000
Subject: [PATCH] chore(sonar): clean up maintainability + reliability smells
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Track A — fix in code (the unambiguous mechanical wins):
- 21 reliability-impact smells: replaceAll over replace, role="list"
removed from / (implicit by HTML), Number.parseInt over
parseInt, ambiguous JSX whitespace tightened.
- 19 typescript:S7764: window→globalThis (timer refs typed as
ReturnType to satisfy DOM/Node ambiguity).
- 26 go:S1192: extracted package-local constants for repeated string
literals (.jsonl, Content-Type/application/json/Cache-Control/no-store,
POST only, auth/input log labels, error-message templates,
display-message tmux subcommand, %q-not-found, color-wrap %s%s%s\\n).
- Misc small wins: arr.at(-1) over arr[length-1], optional chaining,
removed unnecessary type assertions, blank-import comment for
go-sqlite3.
Track B — bulk-accept workflow for remaining smells:
.github/workflows/sonar-bulk-accept.yml is a workflow_dispatch job
(default dry_run=true) that calls SonarCloud's bulk_change API to
mark the remaining smells as Accepted with a per-bucket comment:
- typescript:S6759 readonly props (project style)
- typescript:S6819 role=status, S3358 nested ternary, S6571 redundant
type, S6754 useState style, S6479 array-index keys, S3735 void,
S1874 deprecation, S7763 export-from, S7718 set-has, S6772
ambiguous spacing (remaining), S6582 chain (remaining), S4624
nested template literals, S6822 implicit list (remaining), S1871
duplicate case
- godre:S8205 named struct, S8196 interface naming, S8193 receiver,
S8242 ctx field, go:S107/S117 signature
- All test-file CODE_SMELL findings (table-driven density is by design)
- godre:S8239 marked False Positive: shutdown handler intentionally
uses context.Background() because the parent ctx is already Done at
that point (deriving would give zero-grace shutdown).
go build/test green (918 pass, 27 pkgs); UI tsc + vitest green
(206 pass, 29 files).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/workflows/sonar-bulk-accept.yml | 288 ++++++++++++++++++++++++
cmd/attach.go | 38 ++--
cmd/logs.go | 22 +-
cmd/overlay.go | 28 ++-
internal/output/format.go | 17 +-
internal/serve/api/auth.go | 31 ++-
internal/serve/api/feed_history.go | 8 +-
internal/serve/api/handler_helpers.go | 14 +-
internal/serve/api/input.go | 18 +-
internal/serve/api/logs_usage.go | 6 +-
internal/serve/api/mutations.go | 14 +-
internal/serve/api/revert.go | 16 +-
internal/serve/server.go | 43 ++--
internal/serve/store/cost_store.go | 14 +-
internal/session/state.go | 18 +-
internal/tmux/client.go | 10 +-
ui/src/components/AgentTeamsPanel.tsx | 4 +-
ui/src/components/AttentionLabel.tsx | 2 +-
ui/src/components/AuthProvider.tsx | 4 +-
ui/src/components/BashOnlyRow.tsx | 2 +-
ui/src/components/CostChart.tsx | 4 +-
ui/src/components/FeedStream.tsx | 4 +-
ui/src/components/NewSessionModal.tsx | 14 +-
ui/src/components/RevertSheet.tsx | 5 +-
ui/src/components/SessionInputBar.tsx | 8 +-
ui/src/components/SessionListPanel.tsx | 4 +-
ui/src/components/SseProvider.tsx | 8 +-
ui/src/components/SubagentTree.tsx | 2 +-
ui/src/components/TokenBreakdown.tsx | 4 +-
ui/src/components/ToolCallRow.tsx | 4 +-
ui/src/hooks/useHotkey.ts | 4 +-
ui/src/hooks/useTheme.tsx | 6 +-
ui/src/lib/ansi.ts | 7 +-
ui/src/lib/format.ts | 2 +-
ui/src/routes/Dashboard.tsx | 4 +-
ui/src/routes/DoctorPanel.tsx | 4 +-
ui/src/routes/SessionDetail.tsx | 10 +-
37 files changed, 521 insertions(+), 170 deletions(-)
create mode 100644 .github/workflows/sonar-bulk-accept.yml
diff --git a/.github/workflows/sonar-bulk-accept.yml b/.github/workflows/sonar-bulk-accept.yml
new file mode 100644
index 0000000..e7d9df1
--- /dev/null
+++ b/.github/workflows/sonar-bulk-accept.yml
@@ -0,0 +1,288 @@
+name: SonarCloud Bulk Accept
+
+# Marks remaining open code smells in well-defined buckets as
+# Accepted (formerly "Won't Fix") with a deliberate comment, so the
+# Sonar issue list reflects only smells we actually want to act on.
+#
+# Trigger manually from the Actions tab — never runs on push/PR. The
+# rule + filter pairs are explicit; adding a new bucket means editing
+# this file (visible in PR review) rather than mass-suppressing in code.
+#
+# Each bucket sends ONE bulk_change call to the SonarCloud API:
+# POST /api/issues/bulk_change
+# issues=
+# do_transition=accept
+# comment=
+#
+# Why "Accepted" and not "False Positive": these are real findings
+# under their respective rules — we just don't intend to act on them.
+# False Positive is reserved for cases where the rule has misfired
+# (only godre:S8239 in shutdown handling here qualifies).
+
+on:
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: "Print buckets and counts without calling the API"
+ type: boolean
+ default: true
+
+jobs:
+ bulk-accept:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ env:
+ SONAR_HOST: https://sonarcloud.io
+ SONAR_PROJECT: RandomCodeSpace_ctm
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ steps:
+ - name: Verify SONAR_TOKEN
+ run: |
+ if [ -z "${SONAR_TOKEN}" ]; then
+ echo "::error::SONAR_TOKEN secret is not set"
+ exit 1
+ fi
+
+ - name: Install jq
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
+
+ - name: Process buckets
+ env:
+ DRY_RUN: ${{ inputs.dry_run }}
+ run: |
+ set -euo pipefail
+
+ # Helper: fetch all open CODE_SMELL issue keys matching a filter
+ # (rule + optional path glob via componentKeys). Paginates.
+ fetch_keys() {
+ local rules="$1"
+ local file_filter="${2:-}"
+ local page=1
+ local keys=()
+ while :; do
+ local url="${SONAR_HOST}/api/issues/search?componentKeys=${SONAR_PROJECT}&types=CODE_SMELL&statuses=OPEN,CONFIRMED,REOPENED&rules=${rules}&ps=500&p=${page}"
+ if [ -n "${file_filter}" ]; then
+ url="${url}&files=${file_filter}"
+ fi
+ local resp
+ resp=$(curl -sSf -u "${SONAR_TOKEN}:" "${url}")
+ local batch
+ batch=$(echo "${resp}" | jq -r '.issues[].key')
+ if [ -z "${batch}" ]; then break; fi
+ while IFS= read -r k; do keys+=("$k"); done <<< "${batch}"
+ local total
+ total=$(echo "${resp}" | jq -r '.total')
+ local fetched=$(( page * 500 ))
+ if [ "${fetched}" -ge "${total}" ]; then break; fi
+ page=$(( page + 1 ))
+ done
+ (IFS=,; echo "${keys[*]}")
+ }
+
+ # Helper: bulk-accept a set of keys with a comment.
+ bulk_accept() {
+ local label="$1"
+ local keys="$2"
+ local comment="$3"
+ if [ -z "${keys}" ]; then
+ echo "[${label}] no matching issues — skipping"
+ return
+ fi
+ local count
+ count=$(echo "${keys}" | tr ',' '\n' | wc -l)
+ echo "[${label}] ${count} issues"
+ if [ "${DRY_RUN}" = "true" ]; then
+ echo "[${label}] DRY_RUN — not calling bulk_change"
+ return
+ fi
+ # SonarCloud bulk_change accepts at most ~500 keys per call.
+ # Split into chunks of 400 to stay safe.
+ local chunk
+ local rest="${keys}"
+ while [ -n "${rest}" ]; do
+ chunk=$(echo "${rest}" | cut -d',' -f1-400)
+ rest=$(echo "${rest}" | cut -d',' -f401- || true)
+ if [ "${rest}" = "${chunk}" ]; then rest=""; fi
+ curl -sSf -u "${SONAR_TOKEN}:" -X POST \
+ --data-urlencode "issues=${chunk}" \
+ --data-urlencode "do_transition=accept" \
+ --data-urlencode "comment=${comment}" \
+ "${SONAR_HOST}/api/issues/bulk_change" > /dev/null
+ done
+ echo "[${label}] accepted"
+ }
+
+ # ─────────────────────────────────────────────────────────────
+ # Bucket 1: typescript:S6759 — "Mark props as read-only"
+ # The codebase deliberately does not adopt Readonly; this
+ # is a project-wide style choice, not a per-component miss.
+ KEYS=$(fetch_keys "typescript:S6759")
+ bulk_accept "S6759 readonly-props" "${KEYS}" \
+ "Project style: props interfaces are not wrapped in Readonly<>. Deliberate — accepted."
+
+ # Bucket 2: typescript:S6819 — "Use
)}
-
+
{teams.map((team) => (
-
@@ -108,7 +108,7 @@ function TeamCard({ team }: { team: Team }) {
{team.summary}
)}
-
+
{team.members.map((m) => (
))}
diff --git a/ui/src/components/AttentionLabel.tsx b/ui/src/components/AttentionLabel.tsx
index f9eeae1..7b6e91e 100644
--- a/ui/src/components/AttentionLabel.tsx
+++ b/ui/src/components/AttentionLabel.tsx
@@ -17,7 +17,7 @@ const HUMAN: Record = {
};
function humanize(state: string): string {
- return HUMAN[state] ?? state.replace(/_/g, " ");
+ return HUMAN[state] ?? state.replaceAll("_", " ");
}
interface AttentionLabelProps {
diff --git a/ui/src/components/AuthProvider.tsx b/ui/src/components/AuthProvider.tsx
index e309cca..dd93aca 100644
--- a/ui/src/components/AuthProvider.tsx
+++ b/ui/src/components/AuthProvider.tsx
@@ -48,8 +48,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const onStorage = (e: StorageEvent) => {
if (e.key === TOKEN_KEY) setTokenState(e.newValue);
};
- window.addEventListener("storage", onStorage);
- return () => window.removeEventListener("storage", onStorage);
+ globalThis.addEventListener("storage", onStorage);
+ return () => globalThis.removeEventListener("storage", onStorage);
}, []);
// Subscribe to TanStack Query failures — 401s from any query trigger sign-out.
diff --git a/ui/src/components/BashOnlyRow.tsx b/ui/src/components/BashOnlyRow.tsx
index ed94832..2fbe8c5 100644
--- a/ui/src/components/BashOnlyRow.tsx
+++ b/ui/src/components/BashOnlyRow.tsx
@@ -28,7 +28,7 @@ function truncate(s: string, max: number): string {
export function BashOnlyRow({ row }: BashOnlyRowProps) {
const [open, setOpen] = useState(false);
- const cmdFull = stripAnsi(row.input ?? "").replace(/\s+/g, " ").trim();
+ const cmdFull = stripAnsi(row.input ?? "").replaceAll(/\s+/g, " ").trim();
const cmdLine = truncate(cmdFull, CMD_MAX);
const hasExit = typeof row.exit_code === "number";
diff --git a/ui/src/components/CostChart.tsx b/ui/src/components/CostChart.tsx
index 416413c..f6216d2 100644
--- a/ui/src/components/CostChart.tsx
+++ b/ui/src/components/CostChart.tsx
@@ -51,9 +51,9 @@ export function CostChart({ sessionName, className }: Props) {
series.push({ ts: Date.parse(p.ts), cum });
}
const firstTs = series[0].ts;
- const lastTs = series[series.length - 1].ts;
+ const lastTs = series.at(-1)!.ts;
const spanMs = Math.max(1, lastTs - firstTs);
- const peak = series[series.length - 1].cum || 1;
+ const peak = series.at(-1)!.cum || 1;
const pts = series.map((s) => {
const x = PADDING.left + ((s.ts - firstTs) / spanMs) * INNER_W;
diff --git a/ui/src/components/FeedStream.tsx b/ui/src/components/FeedStream.tsx
index 1ad8e7f..87ae775 100644
--- a/ui/src/components/FeedStream.tsx
+++ b/ui/src/components/FeedStream.tsx
@@ -99,7 +99,7 @@ export function FeedStream({
// hasn't delivered yet), fall back to "now" so the first click
// still asks the server for anything older than the present
// moment — it's a best-effort upper bound.
- const oldest = rows[rows.length - 1];
+ const oldest = rows.at(-1);
const cursor = oldest
? cursorFromRow(oldest)
: `${BigInt(Date.now()) * 1_000_000n}-0`;
@@ -143,7 +143,7 @@ export function FeedStream({
{emptyMessage}
) : (
-
+
{rows.map((row, i) => (
-
{bashOnly ? (
diff --git a/ui/src/components/NewSessionModal.tsx b/ui/src/components/NewSessionModal.tsx
index cff053b..4bc706d 100644
--- a/ui/src/components/NewSessionModal.tsx
+++ b/ui/src/components/NewSessionModal.tsx
@@ -128,7 +128,7 @@ export function NewSessionModal({ open, onClose, recents }: NewSessionModalProps
{recents.length > 0 && (
Recents
-
+
{recents.map((r) => (
-