Skip to content

Commit e917178

Browse files
committed
fix: address CodeRabbit findings and add required changelog entry
- web config API: log get_version() failures instead of swallowing them, so an operator can see when the version banner falls back to empty - usePythinkerVersion: reset the shared promise and log on a failed/empty fetch so a transient error no longer permanently disables the backend version banner for the session - test_web_origins: rename unused *args to *_args to signal intent - CHANGELOG: add the missing ## Unreleased entry for this PR's web fixes (unblocks the required changelog-entry-required check) - AGENTS.md: document the changelog-entry-before-PR requirement as a gotcha to stop this check repeatedly blocking PRs
1 parent f531418 commit e917178

5 files changed

Lines changed: 20 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ subagents, skills, web/visualization UIs, and multi-provider LLM authentication.
4747
- **Do not manually edit auto-synced changelog files.** `docs/en/release-notes/changelog.md` is
4848
generated from the root `CHANGELOG.md`; edit `CHANGELOG.md` and run `npm run sync` from `docs/`
4949
instead of hand-editing the generated docs changelog.
50+
- **Before opening any PR that touches shipped code, add a `## Unreleased` entry to `CHANGELOG.md`.**
51+
The required `changelog-entry-required` check fails a PR that changes shipped paths (`src/*`,
52+
`packages/*`, installers, release/installer workflows, `pythinker.spec`) but adds no new non-blank
53+
line under the `## Unreleased` heading — and this has repeatedly blocked PRs. Add a `- ...` bullet
54+
describing the user-facing change up front. Only skip via the `no-changelog` label or
55+
`[skip changelog]` in the PR body when the change is genuinely user-invisible.
5056
- **When working on a PR or GitHub Actions failure, investigate and identify the root cause first.**
5157
Provide the best-practice, most robust design solution; never provide fast fixes or workarounds.
5258
This is a hard constraint.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
1515

1616
## Unreleased
1717

18+
- **Web: same-origin WebSockets accepted, version banner synced to the backend, and token bootstrap race fixed.** The local-mode web server now auto-populates the allowed-origin list (an empty allowlist rejects every `Origin`-bearing request, which previously broke all WebSocket handshakes with a 403). The UI version banner prefers the version the running backend reports (via the config API) over the stale build-time constant, and a transient version-fetch failure no longer permanently disables the backend banner for the session. The initial auth-token bootstrap race that could fail the first request is resolved. `ESC` now reliably terminates only the background tasks spawned by the interrupted turn, and recall context is re-framed so prior-session snippets can't be misread as new instructions.
1819
- **Deep-audit remediation: security, correctness, and multi-instance robustness.** Permission gate: awk programs that shell out via `print | "cmd"` / `getline` are now classified as mutating AND destructive (previously only `system(`/`>` and only mutating), and `xargs -L N` no longer hides its payload from classification. Glob resolves symlinks before its workspace-boundary check (an in-workspace symlink could previously list outside content); progress-note titles are ANSI-sanitized like every other transcript field. Grep content lines are parsed with unambiguous field separators, so paths like `utf-8-codec.py` are no longer mangled with `-n=false` and sensitive-file attribution is exact. Multi-line edits on CRLF files work again (LF-joined old strings are CRLF-translated when needed). `/import` preserves paths byte-for-byte (only a standalone leading/trailing `--force` is treated as the flag). Post-compaction file reminders include `--add-dir` files. Double-interrupt can no longer orphan the interruption-marker write (unanswered tool_calls). Background web replay falls back to full history (not empty) when the watermark stat fails, and a malformed Agent resume id returns a clean "Agent not found". OAuth: login fails loud when the token response lacks a `refresh_token`; a refresh response without `expires_in` carries the previous lifetime forward instead of refreshing every tick; the device-id file can no longer be read empty mid-creation. A failed `theme="auto"` background probe can be retried by re-selecting auto via `/theme`. Multi-instance: sessions now take a per-session writer lock (a second `pythinker -r <id>`/web worker on the same session is refused instead of interleaving turns), the shared `pythinker.json` index uses a locked read-modify-write (no more lost work-dir registrations), JSONL appenders repair torn final lines after a crash, forks materialize atomically, project-memory mutations abort on read failure instead of wiping the file, the journal is capped at 100 recaps, inbox approve/reject claims candidates atomically, and recall re-arms when another instance writes new memory. Subagents: a failed summary continuation no longer discards a completed agent's work, hallucinated subagent types fail fast with the valid-type list (before any RunAgents child launches), background failures carry an `Agent ID:` + resume hint, and a crash inside the runner's own error handling is logged instead of silently lost.
1920
- **Breaking (CLI flags): `pythinker web` / `pythinker vis` host short flag is now `-H`.** `-h` is a help alias on both subcommands (matching the root CLI); previously `-h <ip>` bound the host. Scripts using `-h 0.0.0.0` now print help and exit 0 without starting a server — switch to `-H <ip>` or `--host <ip>`. Part of the security/correctness audit (which also confined Grep to the workspace, gated non-HTTPS provider URLs in the web config API to loopback, and stopped saving OpenAI keys on 401/403).
2021
- **Thinking effort moved to a single top-right label on the input border.** The input box border is now one static frame grey at every effort level instead of recoloring the whole bar cold→hot, and the effort is no longer duplicated in the footer line. It's shown once, as a small label flushed to the right of the input's top border — a level-colored dot (slate→blue→teal→amber→orange→red as `off→max`) plus the muted level word — so the dial stays glanceable without tinting the typing area or cluttering the footer. The label is hidden entirely for native-thinking models (`always_thinking`, no user dial) and non-thinking models, and the rule auto-shortens by the label width so the line never wraps.

src/pythinker_code/web/api/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ def _build_global_config() -> GlobalConfig:
120120

121121
try:
122122
cli_version = get_version()
123-
except Exception:
123+
except Exception as e:
124+
# Non-fatal: the version banner falls back to "", but surface the
125+
# failure so an operator can see get_version() broke (matches the
126+
# logger.warning convention used by the config.toml handlers below).
127+
logger.warning(f"Failed to get CLI version: {e}", exc_info=True)
124128
cli_version = ""
125129

126130
return GlobalConfig(

tests/web/test_web_origins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_local_mode_populates_allowed_origins(
5151
) -> None:
5252
captured_port: dict[str, int] = {}
5353

54-
def fake_uvicorn_run(*args: object, **kwargs: object) -> None:
54+
def fake_uvicorn_run(*_args: object, **kwargs: object) -> None:
5555
captured_port["port"] = int(kwargs["port"]) # type: ignore[arg-type]
5656

5757
monkeypatch.setattr("uvicorn.run", fake_uvicorn_run)

web/src/hooks/usePythinkerVersion.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ async function fetchServerVersion(): Promise<string | null> {
1111
try {
1212
const config = await apiClient.config.getGlobalConfigApiConfigGet();
1313
return config.version || null;
14-
} catch {
14+
} catch (error) {
15+
console.warn("Failed to fetch backend version:", error);
1516
return null;
1617
}
1718
}
@@ -33,6 +34,11 @@ export function usePythinkerVersion(): string {
3334
if (!cancelled) {
3435
setVersion(serverVersion);
3536
}
37+
} else {
38+
// Failed or empty fetch: clear the shared promise so a later mount
39+
// retries instead of reusing a permanently-failed result for the
40+
// rest of the session.
41+
serverVersionPromise = null;
3642
}
3743
});
3844
return () => {

0 commit comments

Comments
 (0)