diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 41b29994..53bb8e23 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,7 +36,7 @@ updates: - package-ecosystem: "npm" directories: - "/web" - - "/vis" + - "/dashboard" - "/docs" - "/packages/install-counter-worker" - "/examples/feedback-worker" @@ -54,7 +54,7 @@ updates: - "patch" ignore: # TypeScript 6.x is a major bump: it deprecates the tsconfig `baseUrl` - # option (TS5101) and breaks the vis/ and web/ builds. Adopting it is a + # option (TS5101) and breaks the dashboard/ and web/ builds. Adopting it is a # coordinated monorepo migration, not four split bot PRs. Hold the major # until that lands; minor/patch within the current line still flow. - dependency-name: "typescript" diff --git a/.github/workflows/linux-installer.yml b/.github/workflows/linux-installer.yml index 630c8557..e4c9db5e 100644 --- a/.github/workflows/linux-installer.yml +++ b/.github/workflows/linux-installer.yml @@ -62,7 +62,7 @@ jobs: cache: "npm" cache-dependency-path: web/package-lock.json - # The web/vis frontends are gitignored build artifacts; without these + # The web/dashboard frontends are gitignored build artifacts; without these # steps PyInstaller froze a binary whose web UI 404'd on "/". The # bundles are arch-independent, so they are built once on the host — # the QEMU container build picks them up from the mounted checkout. @@ -73,8 +73,8 @@ jobs: PYTHINKER_WEB_EXPECT_VERSION: ${{ steps.ver.outputs.version }} run: python3 scripts/build_web.py - - name: Build vis UI bundle - run: python3 scripts/build_vis.py + - name: Build dashboard UI bundle + run: python3 scripts/build_dashboard.py - name: Set up QEMU (aarch64 only) if: matrix.qemu diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml index cd9cbcd4..a0194cfa 100644 --- a/.github/workflows/windows-installer.yml +++ b/.github/workflows/windows-installer.yml @@ -61,7 +61,7 @@ jobs: cache: "npm" cache-dependency-path: web/package-lock.json - # The web/vis frontends are gitignored build artifacts; without these + # The web/dashboard frontends are gitignored build artifacts; without these # steps PyInstaller froze a binary whose web UI 404'd on "/". The spec # file now also refuses to freeze when the bundles are missing. - name: Build web UI bundle @@ -70,8 +70,8 @@ jobs: PYTHINKER_WEB_EXPECT_VERSION: ${{ steps.ver.outputs.version }} run: python scripts/build_web.py - - name: Build vis UI bundle - run: python scripts/build_vis.py + - name: Build dashboard UI bundle + run: python scripts/build_dashboard.py - name: Install PyInstaller run: uv pip install pyinstaller diff --git a/.gitignore b/.gitignore index 3baff9b8..40fd9d9e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,8 @@ src/pythinker_code/deps/tmp # Web build artifacts src/pythinker_code/web/static/assets/ -# Vis build artifacts -src/pythinker_code/vis/static/ +# Dashboard build artifacts +src/pythinker_code/dashboard/static/ # Generated reports .firecrawl/ diff --git a/AGENTS.md b/AGENTS.md index c13115df..6db2d831 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,8 +144,8 @@ Development servers: ```bash make web-back # FastAPI web backend on port 5494 make web-front # web frontend dev server -make vis-back # visualization backend on port 5495 -make vis-front # visualization frontend dev server +make dashboard-back # dashboard backend on port 5495 +make dashboard-front # dashboard frontend dev server ``` Targeted package commands: @@ -173,7 +173,7 @@ Pick the smallest reliable gate for the change, then run broader gates before re | `packages/pythinker-host` | `make check-pythinker-host && make test-pythinker-host` | | `packages/pythinker-review` | `make check-pythinker-review && make test-pythinker-review` | | `sdks/pythinker-sdk` | `make check-pythinker-sdk && make test-pythinker-sdk` | -| Web / vis frontends | `make check-web`; build affected frontend when packaging assets changed | +| Web / dashboard frontends | `make check-web`; build affected frontend when packaging assets changed | | Release / packaging / PyInstaller | `make build` or `make build-bin` as appropriate | If a gate cannot run because of missing system tools (for example `npm`), report that explicitly @@ -221,7 +221,7 @@ see `docs/en/customization/architecture.md`. This list is a quick orientation on - `src/pythinker_code/auth/`: OAuth/API-key provider integrations. - `src/pythinker_code/background/`: background task worker/runtime support. - `src/pythinker_code/cli/`: Typer command tree (lazy-loaded subcommands `mcp`, `plugin`, - `skill`, `web`, `vis`, `info`, `export`, `review`, `secscan`, `security-scan`, `debug`, + `skill`, `web`, `dashboard`, `info`, `export`, `review`, `secscan`, `security-scan`, `debug`, `update`, plus eager `login`, `logout`, `term`, `acp`). - `src/pythinker_code/hooks/`: hook definitions and execution engine. - `src/pythinker_code/plugin/`: plugin discovery and installation support. @@ -239,8 +239,8 @@ see `docs/en/customization/architecture.md`. This list is a quick orientation on - `src/pythinker_code/tools/`: built-in tools (`agent`, `ask_user`, `background`, `dmail`, `file`, `plan`, `shell`, `think`, `todo`, `web`, etc.). - `src/pythinker_code/ui/`: shell, print, and ACP frontends. -- `src/pythinker_code/web/`, `src/pythinker_code/vis/`: backend integrations for web/visualization. -- `web/`, `vis/`: frontend apps bundled into the CLI package. +- `src/pythinker_code/web/`, `src/pythinker_code/dashboard/`: backend integrations for web/visualization. +- `web/`, `dashboard/`: frontend apps bundled into the CLI package. - `packages/pythinker-core/`: LLM abstraction layer for messages, providers, streaming, and tools. - `packages/pythinker-host/`: host abstraction for local/remote file and shell operations. - `packages/pythinker-review/`: review/debug/security engine, code-reviewr-derived PR diff --git a/CHANGELOG.md b/CHANGELOG.md index b97b25bd..2f13790a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ GitHub Releases page; `0.8.0` is the new starting line. ## Unreleased +- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`). + ## 0.44.0 (2026-06-13) - **Toggle auto-update from the CLI.** Running `/update` now opens a menu — *Check for updates now* (the default, so a bare `/update` + Enter still checks immediately) or *Auto-update on startup* with its current state — so the toggle is discoverable without knowing a subcommand. `/update auto on|off` still sets it directly, and `/update auto` with no value opens an interactive On/Off picker (cursor defaulted to the current setting). The same toggle appears in the interactive `/settings` panel, and `pythinker info` reports the auto-update status. All surfaces show the *effective* state — an external override (`PYTHINKER_CLI_NO_AUTO_UPDATE` or a source checkout) is surfaced as the reason, renders the `/settings` row read-only, and makes `/update auto` report the read-only state rather than popping a no-op picker, so the toggle is never a silent no-op. diff --git a/Makefile b/Makefile index e55076dd..c0469e49 100644 --- a/Makefile +++ b/Makefile @@ -29,12 +29,12 @@ web-back: ## Start web backend with uvicorn (reload enabled). web-front: ## Start web frontend (vite dev server). @npm --prefix web run dev -# for pythinker vis development -.PHONY: vis-back vis-front -vis-back: ## Start vis backend with uvicorn (reload enabled). - @LOG_LEVEL=DEBUG uv run uvicorn pythinker_code.vis.app:create_app --factory --reload --port 5495 -vis-front: ## Start vis frontend (vite dev server). - @npm --prefix vis run dev +# for pythinker dashboard development +.PHONY: dashboard-back dashboard-front +dashboard-back: ## Start dashboard backend with uvicorn (reload enabled). + @LOG_LEVEL=DEBUG uv run uvicorn pythinker_code.dashboard.app:create_app --factory --reload --port 5495 +dashboard-front: ## Start dashboard frontend (vite dev server). + @npm --prefix dashboard run dev .PHONY: format format-pythinker-code format-pythinker-core format-pythinker-host format-pythinker-review format-pythinker-sdk format-web format: format-pythinker-code format-pythinker-core format-pythinker-host format-pythinker-review format-pythinker-sdk format-web ## Auto-format all workspace packages. @@ -145,8 +145,8 @@ cov-pythinker-sdk: ## Run Pythinker SDK tests with coverage. --cov --cov-report=xml:coverage.xml -vv .PHONY: build build-pythinker-code build-pythinker-core build-pythinker-host build-pythinker-review build-pythinker-sdk build-bin build-bin-onedir -build: build-web build-vis build-pythinker-code build-pythinker-core build-pythinker-host build-pythinker-review build-pythinker-sdk ## Build Python packages for release. -build-pythinker-code: build-web build-vis ## Build the pythinker-code sdist and wheel. +build: build-web build-dashboard build-pythinker-code build-pythinker-core build-pythinker-host build-pythinker-review build-pythinker-sdk ## Build Python packages for release. +build-pythinker-code: build-web build-dashboard ## Build the pythinker-code sdist and wheel. @echo "==> Building pythinker-code distributions" @uv build --package pythinker-code --no-sources --out-dir dist build-pythinker-core: ## Build the pythinker-core sdist and wheel. @@ -164,15 +164,15 @@ build-pythinker-sdk: ## Build the pythinker-sdk sdist and wheel. build-web: ## Build web UI and sync into pythinker-code package. @echo "==> Building web UI" @uv run scripts/build_web.py -build-vis: ## Build vis UI and sync into pythinker-code package. - @echo "==> Building vis UI" - @uv run scripts/build_vis.py -build-bin: build-web build-vis ## Build the standalone executable with PyInstaller (one-file mode). +build-dashboard: ## Build dashboard UI and sync into pythinker-code package. + @echo "==> Building dashboard UI" + @uv run scripts/build_dashboard.py +build-bin: build-web build-dashboard ## Build the standalone executable with PyInstaller (one-file mode). @echo "==> Building PyInstaller binary (one-file)" @uv run pyinstaller pythinker.spec @mkdir -p dist/onefile @if [ -f dist/pythinker.exe ]; then mv dist/pythinker.exe dist/onefile/; elif [ -f dist/pythinker ]; then mv dist/pythinker dist/onefile/; fi -build-bin-onedir: build-web build-vis ## Build the standalone executable with PyInstaller (one-dir mode). +build-bin-onedir: build-web build-dashboard ## Build the standalone executable with PyInstaller (one-dir mode). @echo "==> Building PyInstaller binary (one-dir)" @rm -rf dist/onedir dist/pythinker @PYINSTALLER_ONEDIR=1 uv run pyinstaller pythinker.spec diff --git a/README.md b/README.md index becf1dce..b9724df9 100644 --- a/README.md +++ b/README.md @@ -744,8 +744,8 @@ make test-pythinker-sdk # SDK only ```sh make web-back # web backend make web-front # web frontend -make vis-back # vis backend -make vis-front # vis frontend +make dashboard-back # dashboard backend +make dashboard-front # dashboard frontend ``` @@ -770,7 +770,7 @@ make help # all targets ``` pythinker-code/ -├── 📦 src/pythinker_code/ CLI runtime · tools · UIs · ACP · MCP · hooks · plugins · skills · web · vis backends +├── 📦 src/pythinker_code/ CLI runtime · tools · UIs · ACP · MCP · hooks · plugins · skills · web · dashboard backends ├── 🧱 packages/ │ ├── pythinker-core/ Provider-agnostic message, tool, and chat-provider abstractions │ ├── pythinker-host/ Local/remote host filesystem and command execution diff --git a/vis/AGENTS.md b/dashboard/AGENTS.md similarity index 57% rename from vis/AGENTS.md rename to dashboard/AGENTS.md index c1849ea1..6458fc2f 100644 --- a/vis/AGENTS.md +++ b/dashboard/AGENTS.md @@ -1,4 +1,4 @@ -# Pythinker Vis UI (vis/) +# Pythinker Dashboard UI (dashboard/) React 19 + Vite + TypeScript session-tracing visualizer, bundled into the `pythinker-code` package. It is a read-only viewer of Wire events, context messages, agent state, and subagent @@ -6,20 +6,20 @@ activity. Run all `npm` commands from this directory. ## Critical invariants -- **`vis/src/lib/api.ts` is hand-written, not generated** (unlike `web/`, which has a generated +- **`dashboard/src/lib/api.ts` is hand-written, not generated** (unlike `web/`, which has a generated client). Keep its types (`WireEvent`, `ContextMessage`, `SessionInfo`, `SubagentInfo`) in - sync by hand with the vis backend (`src/pythinker_code/vis/`) and the Wire protocol + sync by hand with the dashboard backend (`src/pythinker_code/dashboard/`) and the Wire protocol (`src/pythinker_code/wire/`) whenever either changes. There is no codegen step here. -- **Do not hand-copy build output.** `scripts/build_vis.py` builds and syncs `vis/dist` → - `src/pythinker_code/vis/static`. No ad-hoc copy/rsync scripts. -- **Read-only data plane.** It consumes `/api/vis/*` from the vis backend (port 5495) with a +- **Do not hand-copy build output.** `scripts/build_dashboard.py` builds and syncs `dashboard/dist` → + `src/pythinker_code/dashboard/static`. No ad-hoc copy/rsync scripts. +- **Read-only data plane.** It consumes `/api/dashboard/*` from the dashboard backend (port 5495) with a Bearer token taken from a URL query param; `cache.ts` dedupes in-flight requests. Do not add write/mutation calls here — mutations belong to the web UI / backend, not the visualizer. ## Stack and conventions -- Build: `tsc -b && vite build`; dev: `npm run dev` (or `make vis-front`). Backend: - `make vis-back` (port 5495). +- Build: `tsc -b && vite build`; dev: `npm run dev` (or `make dashboard-front`). Backend: + `make dashboard-back` (port 5495). - Styling: Tailwind + Radix UI / shadcn; `react-virtuoso` for efficient large-session lists. - Feature panels live under `src/features/` (`wire-viewer`, `context-viewer`, `agents-panel`, `state-viewer`, `sessions-explorer`). diff --git a/vis/components.json b/dashboard/components.json similarity index 100% rename from vis/components.json rename to dashboard/components.json diff --git a/vis/index.html b/dashboard/index.html similarity index 100% rename from vis/index.html rename to dashboard/index.html diff --git a/vis/package-lock.json b/dashboard/package-lock.json similarity index 99% rename from vis/package-lock.json rename to dashboard/package-lock.json index bc5319f5..4160b8ed 100644 --- a/vis/package-lock.json +++ b/dashboard/package-lock.json @@ -1,11 +1,11 @@ { - "name": "pythinker-vis", + "name": "pythinker-dashboard", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pythinker-vis", + "name": "pythinker-dashboard", "version": "0.0.0", "dependencies": { "@fontsource-variable/inter": "^5.2.8", @@ -668,33 +668,10 @@ "@noble/ciphers": "^1.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -2804,6 +2781,37 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", @@ -3072,6 +3080,47 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", @@ -10212,6 +10261,23 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dependencies": { + "tslib": "^2.4.0" + } } } } diff --git a/vis/package.json b/dashboard/package.json similarity index 97% rename from vis/package.json rename to dashboard/package.json index 2a2f7a89..51547e44 100644 --- a/vis/package.json +++ b/dashboard/package.json @@ -1,5 +1,5 @@ { - "name": "pythinker-vis", + "name": "pythinker-dashboard", "private": true, "version": "0.0.0", "type": "module", diff --git a/vis/src/App.tsx b/dashboard/src/App.tsx similarity index 97% rename from vis/src/App.tsx rename to dashboard/src/App.tsx index 8de1fda4..0a6e5977 100644 --- a/vis/src/App.tsx +++ b/dashboard/src/App.tsx @@ -13,7 +13,7 @@ import { type WireEvent, getSessionDownloadUrl, getSubagents, - getVisCapabilities, + getDashboardCapabilities, getWireEvents, listSessions, openInPath, @@ -342,10 +342,10 @@ export function App() { listSessions().then(setSessions).catch(() => {}); }, []); useEffect(() => { - getVisCapabilities() + getDashboardCapabilities() .then((capabilities) => setOpenInSupported(capabilities.open_in_supported)) .catch((error) => { - console.error("Failed to load vis capabilities:", error); + console.error("Failed to load dashboard capabilities:", error); setOpenInSupported(false); }); }, []); @@ -381,6 +381,9 @@ export function App() { return; } + // Tab shortcuts only apply when a session is open (the tabs are rendered). + if (!sessionId) return; + if (e.key === "1") setActiveTab("wire"); else if (e.key === "2") setActiveTab("context"); else if (e.key === "3") setActiveTab("state"); @@ -389,7 +392,7 @@ export function App() { }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [showShortcutHelp]); + }, [showShortcutHelp, sessionId]); return (
@@ -613,13 +616,21 @@ export function App() {

Global

+
+ +
+
+ +
+

+ Session Views +

-
diff --git a/vis/src/components/markdown.tsx b/dashboard/src/components/markdown.tsx similarity index 100% rename from vis/src/components/markdown.tsx rename to dashboard/src/components/markdown.tsx diff --git a/vis/src/components/metric-card.tsx b/dashboard/src/components/metric-card.tsx similarity index 100% rename from vis/src/components/metric-card.tsx rename to dashboard/src/components/metric-card.tsx diff --git a/vis/src/components/ui/alert-dialog.tsx b/dashboard/src/components/ui/alert-dialog.tsx similarity index 100% rename from vis/src/components/ui/alert-dialog.tsx rename to dashboard/src/components/ui/alert-dialog.tsx diff --git a/vis/src/components/ui/card.tsx b/dashboard/src/components/ui/card.tsx similarity index 100% rename from vis/src/components/ui/card.tsx rename to dashboard/src/components/ui/card.tsx diff --git a/vis/src/components/ui/select.tsx b/dashboard/src/components/ui/select.tsx similarity index 100% rename from vis/src/components/ui/select.tsx rename to dashboard/src/components/ui/select.tsx diff --git a/vis/src/components/ui/tooltip.tsx b/dashboard/src/components/ui/tooltip.tsx similarity index 100% rename from vis/src/components/ui/tooltip.tsx rename to dashboard/src/components/ui/tooltip.tsx diff --git a/vis/src/features/agents-panel/agent-scope-bar.tsx b/dashboard/src/features/agents-panel/agent-scope-bar.tsx similarity index 100% rename from vis/src/features/agents-panel/agent-scope-bar.tsx rename to dashboard/src/features/agents-panel/agent-scope-bar.tsx diff --git a/vis/src/features/agents-panel/agents-panel.tsx b/dashboard/src/features/agents-panel/agents-panel.tsx similarity index 100% rename from vis/src/features/agents-panel/agents-panel.tsx rename to dashboard/src/features/agents-panel/agents-panel.tsx diff --git a/vis/src/features/context-viewer/assistant-message.tsx b/dashboard/src/features/context-viewer/assistant-message.tsx similarity index 100% rename from vis/src/features/context-viewer/assistant-message.tsx rename to dashboard/src/features/context-viewer/assistant-message.tsx diff --git a/vis/src/features/context-viewer/context-space-map.tsx b/dashboard/src/features/context-viewer/context-space-map.tsx similarity index 100% rename from vis/src/features/context-viewer/context-space-map.tsx rename to dashboard/src/features/context-viewer/context-space-map.tsx diff --git a/vis/src/features/context-viewer/context-viewer.tsx b/dashboard/src/features/context-viewer/context-viewer.tsx similarity index 100% rename from vis/src/features/context-viewer/context-viewer.tsx rename to dashboard/src/features/context-viewer/context-viewer.tsx diff --git a/vis/src/features/context-viewer/tool-call-block.tsx b/dashboard/src/features/context-viewer/tool-call-block.tsx similarity index 100% rename from vis/src/features/context-viewer/tool-call-block.tsx rename to dashboard/src/features/context-viewer/tool-call-block.tsx diff --git a/vis/src/features/context-viewer/user-message.tsx b/dashboard/src/features/context-viewer/user-message.tsx similarity index 100% rename from vis/src/features/context-viewer/user-message.tsx rename to dashboard/src/features/context-viewer/user-message.tsx diff --git a/vis/src/features/dual-view/dual-view.tsx b/dashboard/src/features/dual-view/dual-view.tsx similarity index 100% rename from vis/src/features/dual-view/dual-view.tsx rename to dashboard/src/features/dual-view/dual-view.tsx diff --git a/vis/src/features/session-picker/session-picker.tsx b/dashboard/src/features/session-picker/session-picker.tsx similarity index 100% rename from vis/src/features/session-picker/session-picker.tsx rename to dashboard/src/features/session-picker/session-picker.tsx diff --git a/vis/src/features/sessions-explorer/explorer-toolbar.tsx b/dashboard/src/features/sessions-explorer/explorer-toolbar.tsx similarity index 100% rename from vis/src/features/sessions-explorer/explorer-toolbar.tsx rename to dashboard/src/features/sessions-explorer/explorer-toolbar.tsx diff --git a/vis/src/features/sessions-explorer/project-group.tsx b/dashboard/src/features/sessions-explorer/project-group.tsx similarity index 100% rename from vis/src/features/sessions-explorer/project-group.tsx rename to dashboard/src/features/sessions-explorer/project-group.tsx diff --git a/vis/src/features/sessions-explorer/session-card.tsx b/dashboard/src/features/sessions-explorer/session-card.tsx similarity index 100% rename from vis/src/features/sessions-explorer/session-card.tsx rename to dashboard/src/features/sessions-explorer/session-card.tsx diff --git a/vis/src/features/sessions-explorer/sessions-explorer.tsx b/dashboard/src/features/sessions-explorer/sessions-explorer.tsx similarity index 100% rename from vis/src/features/sessions-explorer/sessions-explorer.tsx rename to dashboard/src/features/sessions-explorer/sessions-explorer.tsx diff --git a/vis/src/features/state-viewer/state-viewer.tsx b/dashboard/src/features/state-viewer/state-viewer.tsx similarity index 100% rename from vis/src/features/state-viewer/state-viewer.tsx rename to dashboard/src/features/state-viewer/state-viewer.tsx diff --git a/vis/src/features/statistics/statistics-view.tsx b/dashboard/src/features/statistics/statistics-view.tsx similarity index 98% rename from vis/src/features/statistics/statistics-view.tsx rename to dashboard/src/features/statistics/statistics-view.tsx index eb643304..f9c38a86 100644 --- a/vis/src/features/statistics/statistics-view.tsx +++ b/dashboard/src/features/statistics/statistics-view.tsx @@ -78,7 +78,7 @@ function DailyUsageChart({ daily }: { daily: AggregateStats["daily_usage"] }) { const labelStep = Math.max(1, Math.ceil(daily.length / 6)); return ( - +
Daily Usage @@ -333,7 +333,7 @@ export function StatisticsView() {
))}
-
+
diff --git a/vis/src/features/usage/usage-heatmap.tsx b/dashboard/src/features/usage/usage-heatmap.tsx similarity index 100% rename from vis/src/features/usage/usage-heatmap.tsx rename to dashboard/src/features/usage/usage-heatmap.tsx diff --git a/vis/src/features/usage/usage-trend-chart.tsx b/dashboard/src/features/usage/usage-trend-chart.tsx similarity index 100% rename from vis/src/features/usage/usage-trend-chart.tsx rename to dashboard/src/features/usage/usage-trend-chart.tsx diff --git a/vis/src/features/usage/usage-view.tsx b/dashboard/src/features/usage/usage-view.tsx similarity index 100% rename from vis/src/features/usage/usage-view.tsx rename to dashboard/src/features/usage/usage-view.tsx diff --git a/vis/src/features/wire-viewer/decision-path.tsx b/dashboard/src/features/wire-viewer/decision-path.tsx similarity index 100% rename from vis/src/features/wire-viewer/decision-path.tsx rename to dashboard/src/features/wire-viewer/decision-path.tsx diff --git a/vis/src/features/wire-viewer/integrity-check.tsx b/dashboard/src/features/wire-viewer/integrity-check.tsx similarity index 100% rename from vis/src/features/wire-viewer/integrity-check.tsx rename to dashboard/src/features/wire-viewer/integrity-check.tsx diff --git a/vis/src/features/wire-viewer/timeline-view.tsx b/dashboard/src/features/wire-viewer/timeline-view.tsx similarity index 100% rename from vis/src/features/wire-viewer/timeline-view.tsx rename to dashboard/src/features/wire-viewer/timeline-view.tsx diff --git a/vis/src/features/wire-viewer/tool-call-detail.tsx b/dashboard/src/features/wire-viewer/tool-call-detail.tsx similarity index 100% rename from vis/src/features/wire-viewer/tool-call-detail.tsx rename to dashboard/src/features/wire-viewer/tool-call-detail.tsx diff --git a/vis/src/features/wire-viewer/tool-stats-dashboard.tsx b/dashboard/src/features/wire-viewer/tool-stats-dashboard.tsx similarity index 100% rename from vis/src/features/wire-viewer/tool-stats-dashboard.tsx rename to dashboard/src/features/wire-viewer/tool-stats-dashboard.tsx diff --git a/vis/src/features/wire-viewer/turn-efficiency.tsx b/dashboard/src/features/wire-viewer/turn-efficiency.tsx similarity index 100% rename from vis/src/features/wire-viewer/turn-efficiency.tsx rename to dashboard/src/features/wire-viewer/turn-efficiency.tsx diff --git a/vis/src/features/wire-viewer/turn-tree.tsx b/dashboard/src/features/wire-viewer/turn-tree.tsx similarity index 97% rename from vis/src/features/wire-viewer/turn-tree.tsx rename to dashboard/src/features/wire-viewer/turn-tree.tsx index 5889395c..ffdfa960 100644 --- a/vis/src/features/wire-viewer/turn-tree.tsx +++ b/dashboard/src/features/wire-viewer/turn-tree.tsx @@ -99,7 +99,12 @@ function buildTree(events: WireEvent[]): TurnNode[] { if (tc) tc.hasError = true; } } else if (event.type === "SubagentEvent" && currentStep) { - const taskId = event.payload.parent_tool_call_id as string ?? ""; + // Group by parent tool-call id; fall back to agent id, then to a per-event + // key so distinct keyless subagent events are not merged under one bucket. + const taskId = + (event.payload.parent_tool_call_id as string | undefined) ?? + (event.payload.agent_id as string | undefined) ?? + `idx:${event.index}`; const inner = event.payload.event as Record | undefined; const innerType = (inner?.type as string) ?? ""; const innerPayload = (inner?.payload as Record) ?? {}; diff --git a/vis/src/features/wire-viewer/usage-chart.tsx b/dashboard/src/features/wire-viewer/usage-chart.tsx similarity index 100% rename from vis/src/features/wire-viewer/usage-chart.tsx rename to dashboard/src/features/wire-viewer/usage-chart.tsx diff --git a/vis/src/features/wire-viewer/wire-event-card.tsx b/dashboard/src/features/wire-viewer/wire-event-card.tsx similarity index 100% rename from vis/src/features/wire-viewer/wire-event-card.tsx rename to dashboard/src/features/wire-viewer/wire-event-card.tsx diff --git a/vis/src/features/wire-viewer/wire-filters.tsx b/dashboard/src/features/wire-viewer/wire-filters.tsx similarity index 100% rename from vis/src/features/wire-viewer/wire-filters.tsx rename to dashboard/src/features/wire-viewer/wire-filters.tsx diff --git a/vis/src/features/wire-viewer/wire-viewer.tsx b/dashboard/src/features/wire-viewer/wire-viewer.tsx similarity index 100% rename from vis/src/features/wire-viewer/wire-viewer.tsx rename to dashboard/src/features/wire-viewer/wire-viewer.tsx diff --git a/vis/src/hooks/use-theme.ts b/dashboard/src/hooks/use-theme.ts similarity index 66% rename from vis/src/hooks/use-theme.ts rename to dashboard/src/hooks/use-theme.ts index 544d1398..1cf5ea8a 100644 --- a/vis/src/hooks/use-theme.ts +++ b/dashboard/src/hooks/use-theme.ts @@ -10,13 +10,16 @@ function getSystemTheme(): Theme { export function useTheme() { const [theme, setThemeState] = useState(() => { - const saved = localStorage.getItem("vis-theme") as Theme | null; + // Fall back to the legacy "vis-theme" key so users upgrading from the + // previous visualization UI keep their saved preference. + const saved = (localStorage.getItem("dashboard-theme") ?? + localStorage.getItem("vis-theme")) as Theme | null; return saved ?? getSystemTheme(); }); useEffect(() => { document.documentElement.classList.toggle("dark", theme === "dark"); - localStorage.setItem("vis-theme", theme); + localStorage.setItem("dashboard-theme", theme); }, [theme]); const toggleTheme = useCallback(() => { diff --git a/vis/src/index.css b/dashboard/src/index.css similarity index 100% rename from vis/src/index.css rename to dashboard/src/index.css diff --git a/vis/src/lib/api.ts b/dashboard/src/lib/api.ts similarity index 94% rename from vis/src/lib/api.ts rename to dashboard/src/lib/api.ts index 18bf713a..56e92135 100644 --- a/vis/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -1,6 +1,6 @@ import { apiCache } from "./cache.ts"; -const BASE = "/api/vis"; +const BASE = "/api/dashboard"; const TOKEN = new URLSearchParams(window.location.search).get("token"); /** Simple concurrency limiter for batching API requests. */ @@ -236,7 +236,7 @@ export interface AggregateStats { per_project: { work_dir: string; sessions: number; turns: number }[]; } -export interface VisCapabilities { +export interface DashboardCapabilities { open_in_supported: boolean; } @@ -246,10 +246,10 @@ export function getAggregateStats(forceRefresh = false): Promise return apiCache.get(key, () => fetchJSON("/statistics"), 60_000); } -export function getVisCapabilities(forceRefresh = false): Promise { - const key = "vis-capabilities"; +export function getDashboardCapabilities(forceRefresh = false): Promise { + const key = "dashboard-capabilities"; if (forceRefresh) apiCache.invalidate(key); - return apiCache.get(key, () => fetchJSON("/capabilities"), 60_000); + return apiCache.get(key, () => fetchJSON("/capabilities"), 60_000); } export function getSessionDownloadUrl(sessionId: string): string { @@ -302,7 +302,9 @@ export async function importSession(file: File): Promise<{ session_id: string; w try { const formData = new FormData(); formData.append("file", file); - const res = await fetch(`${BASE}/sessions/import`, { method: "POST", body: formData, signal: controller.signal }); + // Note: do not set Content-Type for FormData — the browser sets the multipart boundary. + const headers = TOKEN ? { Authorization: `Bearer ${TOKEN}` } : undefined; + const res = await fetch(`${BASE}/sessions/import`, { method: "POST", headers, body: formData, signal: controller.signal }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.detail || `Import failed: ${res.status}`); @@ -364,7 +366,8 @@ export async function deleteSession(sessionId: string): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30_000); try { - const res = await fetch(`${BASE}/sessions/${sessionId}`, { method: "DELETE", signal: controller.signal }); + const headers = TOKEN ? { Authorization: `Bearer ${TOKEN}` } : undefined; + const res = await fetch(`${BASE}/sessions/${sessionId}`, { method: "DELETE", headers, signal: controller.signal }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.detail || `Delete failed: ${res.status}`); diff --git a/vis/src/lib/cache.ts b/dashboard/src/lib/cache.ts similarity index 100% rename from vis/src/lib/cache.ts rename to dashboard/src/lib/cache.ts diff --git a/vis/src/lib/utils.ts b/dashboard/src/lib/utils.ts similarity index 100% rename from vis/src/lib/utils.ts rename to dashboard/src/lib/utils.ts diff --git a/vis/src/main.tsx b/dashboard/src/main.tsx similarity index 100% rename from vis/src/main.tsx rename to dashboard/src/main.tsx diff --git a/vis/tsconfig.app.json b/dashboard/tsconfig.app.json similarity index 100% rename from vis/tsconfig.app.json rename to dashboard/tsconfig.app.json diff --git a/vis/tsconfig.json b/dashboard/tsconfig.json similarity index 100% rename from vis/tsconfig.json rename to dashboard/tsconfig.json diff --git a/vis/tsconfig.node.json b/dashboard/tsconfig.node.json similarity index 100% rename from vis/tsconfig.node.json rename to dashboard/tsconfig.node.json diff --git a/vis/vite.config.ts b/dashboard/vite.config.ts similarity index 100% rename from vis/vite.config.ts rename to dashboard/vite.config.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f4d95a2e..af19a5fb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -80,7 +80,7 @@ export default withMermaid(defineConfig({ { text: 'pythinker acp Subcommand', link: '/en/reference/pythinker-acp' }, { text: 'pythinker mcp Subcommand', link: '/en/reference/pythinker-mcp' }, { text: 'pythinker term Subcommand', link: '/en/reference/pythinker-term' }, - { text: 'pythinker vis Subcommand', link: '/en/reference/pythinker-vis' }, + { text: 'pythinker dashboard Subcommand', link: '/en/reference/pythinker-dashboard' }, { text: 'pythinker web Subcommand', link: '/en/reference/pythinker-web' }, { text: 'Slash Commands', link: '/en/reference/slash-commands' }, { text: 'Keyboard Shortcuts', link: '/en/reference/keyboard' }, diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 1b576503..6115ad89 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -9,7 +9,7 @@ This repository uses VitePress for the documentation site. Most pages now contai - Guides: getting-started, use-cases, interaction, sessions, ides, integrations - Customization: mcp, plugins, hooks, skills, agents, print-mode, wire-mode - Configuration: config-files, providers, overrides, env-vars, data-locations - - Reference: pythinker-command, pythinker-info, pythinker-acp, pythinker-mcp, pythinker-term, pythinker-vis, pythinker-web, slash-commands, keyboard + - Reference: pythinker-command, pythinker-info, pythinker-acp, pythinker-mcp, pythinker-term, pythinker-dashboard, pythinker-web, slash-commands, keyboard - FAQ: faq - Release notes: changelog, breaking-changes - Navigation and sidebar are defined in `docs/.vitepress/config.ts`. Any new or renamed page must be wired there. diff --git a/docs/en/configuration/data-locations.md b/docs/en/configuration/data-locations.md index 7c049811..c2d55de2 100644 --- a/docs/en/configuration/data-locations.md +++ b/docs/en/configuration/data-locations.md @@ -23,7 +23,7 @@ Note: `PYTHINKER_SHARE_DIR` only affects the storage location of the runtime dat │ ├── context.jsonl │ ├── wire.jsonl │ └── state.json -├── imported_sessions/ # Imported session data (via pythinker vis) +├── imported_sessions/ # Imported session data (via pythinker dashboard) │ └── / │ ├── context.jsonl │ ├── wire.jsonl diff --git a/docs/en/customization/agent-architecture.md b/docs/en/customization/agent-architecture.md index 4b81500b..51ffe707 100644 --- a/docs/en/customization/agent-architecture.md +++ b/docs/en/customization/agent-architecture.md @@ -35,7 +35,7 @@ flowchart TB Soul[PythinkerSoul\nsoul/pythinkersoul.py] RunSoul[run_soul\nWire + UI task + soul task] Wire[Wire\nraw and merged queues] - UI[Shell, print, ACP, web, vis UI] + UI[Shell, print, ACP, web, dashboard UI] Core[pythinker_core.step] LLM[Chat provider] Toolset[PythinkerToolset] @@ -344,4 +344,4 @@ The agent architecture is easiest to read as four concentric layers: 1. `PythinkerCLI.create` is the composition layer. It wires config, model, runtime services, agent spec, context, hooks, and telemetry. 2. `PythinkerSoul` is the lifecycle layer. It owns turns, steps, checkpoints, compaction, slash commands, dynamic injections, and stop conditions. 3. `pythinker_core.step` is the model/tool orchestration layer. It asks the provider for an assistant message, streams output, executes requested tools through the toolset, and exposes tool results back to the soul. -4. `Wire` is the observation layer. It records and broadcasts what happened so shell, print, ACP, web, vis, and external Wire clients can render or control the run without owning agent logic. +4. `Wire` is the observation layer. It records and broadcasts what happened so shell, print, ACP, web, dashboard, and external Wire clients can render or control the run without owning agent logic. diff --git a/docs/en/customization/architecture.md b/docs/en/customization/architecture.md index 2e1a8dd2..46cdde2d 100644 --- a/docs/en/customization/architecture.md +++ b/docs/en/customization/architecture.md @@ -22,7 +22,7 @@ under `blackbox/` are out of scope and are not part of this map. leaf-first. Subagents inherit the parent's already-merged guidance rather than re-resolving from their own directory. A nested `AGENTS.md` therefore only loads when a session's working directory is inside that subtree, which is why nested guides exist for directories people -actually `cd` into (for example `web/`, `vis/`, `tests_e2e/`) and not for every module. +actually `cd` into (for example `web/`, `dashboard/`, `tests_e2e/`) and not for every module. ## Trust boundaries at a glance @@ -32,7 +32,7 @@ The security-relevant edges of the system, independent of any one subsystem: steering queue), over the Wire JSON-RPC protocol (`src/pythinker_code/wire/server.py`), through the ACP server (`src/pythinker_code/acp/server.py`), via CLI flags (`src/pythinker_code/cli/__init__.py`), from config files (`src/pythinker_code/config.py`), - and over HTTP for the web and vis backends. + and over HTTP for the web and dashboard backends. - **Where untrusted content is parsed.** Model tool-call arguments are validated in `pythinker_core.tooling` (`CallableTool2`); MCP output flows through `pythinker_core.tooling.mcp`; web fetch/search results, file reads, and background task @@ -69,14 +69,14 @@ The end-to-end flow when a session starts and processes a turn: 5. **Tool execution** — `src/pythinker_code/soul/toolset.py:PythinkerToolset` loads built-in and MCP tools, injects dependencies, executes calls, and returns structured results. 6. **Wire and UI** — `src/pythinker_code/soul/run_soul` connects the soul to - `src/pythinker_code/wire/`; Shell, Print, ACP, Web, and Vis frontends consume Wire events. + `src/pythinker_code/wire/`; Shell, Print, ACP, Web, and Dashboard frontends consume Wire events. ## Runtime and entry | Path | Purpose | Key entry points and interfaces | | --- | --- | --- | | `src/pythinker_code/__main__.py` | Process entry. | `main` | -| `src/pythinker_code/cli/` | Typer command tree and UI-mode routing; lazy-loaded subcommands. | `cli`, `pythinker`, `login`, `logout`, `term`, `acp`, lazy group `info`, `export`, `mcp`, `plugin`, `skill`, `review`, `secscan`, `security-scan`, `debug`, `update`, `vis`, `web` | +| `src/pythinker_code/cli/` | Typer command tree and UI-mode routing; lazy-loaded subcommands. | `cli`, `pythinker`, `login`, `logout`, `term`, `acp`, lazy group `info`, `export`, `mcp`, `plugin`, `skill`, `review`, `secscan`, `security-scan`, `debug`, `update`, `dashboard`, `web` | | `src/pythinker_code/app.py` | Builds `PythinkerCLI`, `Runtime`, and `PythinkerSoul`; wires telemetry and frontends. | `PythinkerCLI.create`, `PythinkerCLI.run`, `run_shell` / `run_print` / `run_acp` / `run_wire_stdio` | | `src/pythinker_code/config.py` | Three-scope config resolution (user → project → local TOML) with env overlay and JSON→TOML migration; `SecretStr` fields; scope locks on `api_key`/`providers`/`services`. | `Config`, `load_config`, `save_config`, `get_config_file` | | `src/pythinker_code/llm.py` | Provider/model selection and capability derivation; wires `pythinker-core` backends. | `LLM`, `create_llm`, `augment_provider_with_env_vars`, `derive_model_capabilities` | @@ -213,18 +213,18 @@ Full session lifecycle: `initialize`, `new_session`, `load_session`, `resume_ses is load-bearing for approval gating of persistent-backdoor vectors (`AGENTS.md`, agent specs, `.pythinker` config). -## Web and vis backends and frontends +## Web and dashboard backends and frontends | Path | Purpose | Key entry points and interfaces | | --- | --- | --- | | `src/pythinker_code/web/` | FastAPI backend (port 5494) managing CLI sessions via subprocess workers; bearer-token auth; `/api/*`; sensitive-path restriction. | `create_app`, `run_web_server`, `PythinkerCLIRunner`, `SessionProcess`, `AuthMiddleware` | -| `src/pythinker_code/vis/` | FastAPI read-only tracing/statistics backend (port 5495) for the visualizer. | `create_app`, `run_vis_server` | +| `src/pythinker_code/dashboard/` | FastAPI read-only tracing/statistics backend (port 5495) for the visualizer. | `create_app`, `run_dashboard_server` | | `web/` | React 19 + Vite 8 + TypeScript SPA chat UI; bundled into the package. See `web/AGENTS.md`. | `main.tsx`, `App`, `apiClient`, generated client `src/lib/api/`, `useSessionStream` | -| `vis/` | React 19 + Vite session-tracing visualizer. See `vis/AGENTS.md`. | `main.tsx`, `App`, hand-written `src/lib/api.ts` (`WireEvent`, `ContextMessage`, `SessionInfo`), feature panels under `src/features/` | +| `dashboard/` | React 19 + Vite session-tracing visualizer. See `dashboard/AGENTS.md`. | `main.tsx`, `App`, hand-written `src/lib/api.ts` (`WireEvent`, `ContextMessage`, `SessionInfo`), feature panels under `src/features/` | Both frontends build with `tsc -b && vite build` and are synced into the Python package by `scripts/build_web.py` (`web/dist` → `src/pythinker_code/web/static`) and -`scripts/build_vis.py` (`vis/dist` → `src/pythinker_code/vis/static`). +`scripts/build_dashboard.py` (`dashboard/dist` → `src/pythinker_code/dashboard/static`). ## Workspace packages and SDK @@ -243,7 +243,7 @@ Review artifact commands (`describe`, `improve`/`suggest`, `ask`, `labels`, `cha | Path | Purpose | | --- | --- | -| `tests/` | Unit/integration, organized by subsystem (`auth/`, `acp/`, `core/`, `tools/`, `cli/`, `hooks/`, `background/`, `notifications/`, `telemetry/`, `subagents/`, `ui/`, `vis/`, `web/`, `e2e/`). Shared fixtures (`config`, `llm`, `runtime`, `session`, `tools`) in `tests/conftest.py`; `pytest.ini` sets `asyncio_mode=auto` and excludes `tests_e2e`. | +| `tests/` | Unit/integration, organized by subsystem (`auth/`, `acp/`, `core/`, `tools/`, `cli/`, `hooks/`, `background/`, `notifications/`, `telemetry/`, `subagents/`, `ui/`, `dashboard/`, `web/`, `e2e/`). Shared fixtures (`config`, `llm`, `runtime`, `session`, `tools`) in `tests/conftest.py`; `pytest.ini` sets `asyncio_mode=auto` and excludes `tests_e2e`. | | `tests_e2e/` | End-to-end `pythinker --wire` JSON-RPC tests (W-01…W-42 taxonomy) plus CLI/MCP flows; `wire_helpers.py`, `cassette.py` record/replay; `inline_snapshot` with path normalization. See `tests_e2e/AGENTS.md`. | | `tests_ai/` | Accuracy smoke harness invoking agents via the Harbor framework (`scripts/run.py`). | @@ -251,7 +251,7 @@ Review artifact commands (`describe`, `improve`/`suggest`, `ask`, `labels`, `cha Builds and checks fan out across the workspace from the root `Makefile` (`make prepare` / `format` / `check` / `test` / `ai-test` / `build` / `build-bin`, plus -`web-*` / `vis-*` dev servers and per-package `check-*` / `test-*`). Tooling: `uv` workspace, +`web-*` / `dashboard-*` dev servers and per-package `check-*` / `test-*`). Tooling: `uv` workspace, `ruff` (lint + format), `pyright` (enforced), `ty` (advisory). `scripts/release.py` rewrites versions across the five packages but never pushes `main` or tags. Distribution covers PyInstaller binaries (`pythinker.spec`), native installers (`scripts/install-native.sh`, diff --git a/docs/en/reference/pythinker-command.md b/docs/en/reference/pythinker-command.md index de2aed04..a6147058 100644 --- a/docs/en/reference/pythinker-command.md +++ b/docs/en/reference/pythinker-command.md @@ -169,7 +169,7 @@ When not specified, Pythinker Code automatically discovers user-level and projec | [`pythinker plugin`](../customization/plugins.md) | Manage plugins (Beta) | | [`pythinker term`](./pythinker-term.md) | Launch the Toad terminal UI | | [`pythinker export`](#pythinker-export) | Export a session as a ZIP file | -| [`pythinker vis`](./pythinker-vis.md) | Launch the Agent Tracing Visualizer (Technical Preview) | +| [`pythinker dashboard`](./pythinker-dashboard.md) | Launch the Agent Tracing Visualizer (Technical Preview) | | [`pythinker web`](./pythinker-web.md) | Start the Web UI server | ### `pythinker login` @@ -207,7 +207,7 @@ pythinker export [] [-o ] [--yes] Added in version 1.20. ::: -### `pythinker vis` +### `pythinker dashboard` ::: warning Note Technical Preview feature, may be unstable. @@ -216,7 +216,7 @@ Technical Preview feature, may be unstable. Launch the Agent Tracing Visualizer to view and analyze session traces in a browser. ```sh -pythinker vis [OPTIONS] +pythinker dashboard [OPTIONS] ``` | Option | Short | Description | @@ -227,7 +227,7 @@ pythinker vis [OPTIONS] | `--open / --no-open` | | Automatically open browser (default: enabled) | | `--reload` | | Enable auto-reload (development mode) | -See [Agent Tracing Visualizer](./pythinker-vis.md) for details. +See [Agent Tracing Visualizer](./pythinker-dashboard.md) for details. ### `pythinker web` diff --git a/docs/en/reference/pythinker-vis.md b/docs/en/reference/pythinker-dashboard.md similarity index 95% rename from docs/en/reference/pythinker-vis.md rename to docs/en/reference/pythinker-dashboard.md index 710fcc9c..c8693b94 100644 --- a/docs/en/reference/pythinker-vis.md +++ b/docs/en/reference/pythinker-dashboard.md @@ -8,10 +8,10 @@ Agent Tracing Visualizer is a browser-based visualization dashboard for inspecti ## Launch -Run `pythinker vis` in the terminal to start the Visualizer: +Run `pythinker dashboard` in the terminal to start the Visualizer: ```sh -pythinker vis +pythinker dashboard ``` The server automatically opens a browser after startup. The default address is `http://127.0.0.1:5495`. @@ -34,13 +34,13 @@ Examples: ```sh # Specify port -pythinker vis --port 8080 +pythinker dashboard --port 8080 # Don't automatically open browser -pythinker vis --no-open +pythinker dashboard --no-open # Share on LAN (auto-detects and displays LAN IP) -pythinker vis -n +pythinker dashboard -n ``` ## Features diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md index 38d2bbf3..7b367b50 100644 --- a/docs/en/reference/slash-commands.md +++ b/docs/en/reference/slash-commands.md @@ -523,7 +523,7 @@ Switch to Web UI. Pythinker Code will start a Web UI server and open the current ### `/reports` -Open session reports in the Agent Tracing Visualizer. Pythinker Code will start the visualizer server and open the current session's tracing view in the browser, where you can inspect Wire event timelines, context messages, and usage statistics. See [Agent Tracing Visualizer](./pythinker-vis.md) for details. +Open session reports in the Agent Tracing Visualizer (alias: `/dashboard`). Pythinker Code will start the visualizer server and open the current session's tracing view in the browser, where you can inspect Wire event timelines, context messages, and usage statistics. See [Agent Tracing Visualizer](./pythinker-dashboard.md) for details. ## Command completion diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 957c180f..eb6e16ec 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -17,6 +17,8 @@ GitHub Releases page; `0.8.0` is the new starting line. ## Unreleased +- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`). + ## 0.44.0 (2026-06-13) - **Toggle auto-update from the CLI.** Running `/update` now opens a menu — *Check for updates now* (the default, so a bare `/update` + Enter still checks immediately) or *Auto-update on startup* with its current state — so the toggle is discoverable without knowing a subcommand. `/update auto on|off` still sets it directly, and `/update auto` with no value opens an interactive On/Off picker (cursor defaulted to the current setting). The same toggle appears in the interactive `/settings` panel, and `pythinker info` reports the auto-update status. All surfaces show the *effective* state — an external override (`PYTHINKER_CLI_NO_AUTO_UPDATE` or a source checkout) is surfaced as the reason, renders the `/settings` row read-only, and makes `/update auto` report the read-only state rather than popping a no-op picker, so the toggle is never a silent no-op. diff --git a/packages/linux-installer/pythinker.spec b/packages/linux-installer/pythinker.spec index 507e2328..c1781da5 100644 --- a/packages/linux-installer/pythinker.spec +++ b/packages/linux-installer/pythinker.spec @@ -8,7 +8,7 @@ from pathlib import Path from PyInstaller.utils.hooks import collect_data_files, collect_submodules, copy_metadata -# The web/vis frontends are gitignored build artifacts. collect_data_files() +# The web/dashboard frontends are gitignored build artifacts. collect_data_files() # silently collects nothing when they are missing, which shipped installers # whose web UI answered "/" with a 404. Fail the freeze loudly instead. _pkg_spec = importlib.util.find_spec("pythinker_code") @@ -17,13 +17,13 @@ if _pkg_spec is None or _pkg_spec.origin is None: _pkg_root = Path(_pkg_spec.origin).resolve().parent _missing_ui = [ rel - for rel in ("web/static/index.html", "vis/static/index.html") + for rel in ("web/static/index.html", "dashboard/static/index.html") if not (_pkg_root / rel).is_file() ] if _missing_ui: raise SystemExit( f"pythinker.spec: UI bundles missing from pythinker_code ({', '.join(_missing_ui)}). " - "Run `make build-web build-vis` (or scripts/build_web.py and scripts/build_vis.py) " + "Run `make build-web build-dashboard` (or scripts/build_web.py and scripts/build_dashboard.py) " "before freezing, or the packaged web UI will 404 on '/'." ) diff --git a/packages/windows-installer/pythinker.spec b/packages/windows-installer/pythinker.spec index 2aa430f6..43a5b843 100644 --- a/packages/windows-installer/pythinker.spec +++ b/packages/windows-installer/pythinker.spec @@ -7,7 +7,7 @@ from pathlib import Path from PyInstaller.utils.hooks import collect_data_files, collect_submodules, copy_metadata -# The web/vis frontends are gitignored build artifacts. collect_data_files() +# The web/dashboard frontends are gitignored build artifacts. collect_data_files() # silently collects nothing when they are missing, which shipped installers # whose web UI answered "/" with a 404. Fail the freeze loudly instead. _pkg_spec = importlib.util.find_spec("pythinker_code") @@ -16,13 +16,13 @@ if _pkg_spec is None or _pkg_spec.origin is None: _pkg_root = Path(_pkg_spec.origin).resolve().parent _missing_ui = [ rel - for rel in ("web/static/index.html", "vis/static/index.html") + for rel in ("web/static/index.html", "dashboard/static/index.html") if not (_pkg_root / rel).is_file() ] if _missing_ui: raise SystemExit( f"pythinker.spec: UI bundles missing from pythinker_code ({', '.join(_missing_ui)}). " - "Run `make build-web build-vis` (or scripts/build_web.py and scripts/build_vis.py) " + "Run `make build-web build-dashboard` (or scripts/build_web.py and scripts/build_dashboard.py) " "before freezing, or the packaged web UI will 404 on '/'." ) diff --git a/pythinker.spec b/pythinker.spec index 79ea9732..8131647f 100644 --- a/pythinker.spec +++ b/pythinker.spec @@ -3,7 +3,7 @@ import os from pythinker_code.utils.pyinstaller import datas, hiddenimports, require_ui_assets -# Fail loud when the gitignored web/vis bundles haven't been built; a freeze +# Fail loud when the gitignored web/dashboard bundles haven't been built; a freeze # without them ships a binary whose web UI 404s on "/". require_ui_assets() diff --git a/scripts/build_vis.py b/scripts/build_dashboard.py similarity index 67% rename from scripts/build_vis.py rename to scripts/build_dashboard.py index aec1341d..bf9c9c12 100644 --- a/scripts/build_vis.py +++ b/scripts/build_dashboard.py @@ -7,20 +7,20 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -VIS_DIR = ROOT / "vis" -DIST_DIR = VIS_DIR / "dist" -NODE_MODULES = VIS_DIR / "node_modules" -STATIC_DIR = ROOT / "src" / "pythinker_code" / "vis" / "static" +DASHBOARD_DIR = ROOT / "dashboard" +DIST_DIR = DASHBOARD_DIR / "dist" +NODE_MODULES = DASHBOARD_DIR / "node_modules" +STATIC_DIR = ROOT / "src" / "pythinker_code" / "dashboard" / "static" -REQUIRED_VIS_TYPE_FILES = ( +REQUIRED_DASHBOARD_TYPE_FILES = ( NODE_MODULES / "vite" / "client.d.ts", NODE_MODULES / "typescript" / "lib" / "typescript.d.ts", ) -def has_required_vis_type_files() -> bool: - return all(path.is_file() for path in REQUIRED_VIS_TYPE_FILES) +def has_required_dashboard_type_files() -> bool: + return all(path.is_file() for path in REQUIRED_DASHBOARD_TYPE_FILES) def resolve_npm() -> str | None: @@ -51,8 +51,10 @@ def check_node_version() -> bool: file=sys.stderr, ) return False - except Exception: - pass + except Exception as exc: + # Best-effort probe: don't block the build on a probe failure, but make + # the failure visible instead of silently reporting the version as OK. + print(f"Could not verify Node.js version (proceeding anyway): {exc}", file=sys.stderr) return True @@ -71,26 +73,29 @@ def run_npm(npm: str, args: list[str]) -> int: def main() -> int: npm = resolve_npm() if npm is None: - print("npm not found. Install Node.js (npm) to build the vis UI.", file=sys.stderr) + print("npm not found. Install Node.js (npm) to build the dashboard UI.", file=sys.stderr) return 1 if not check_node_version(): return 1 - needs_install = (not NODE_MODULES.exists()) or (not has_required_vis_type_files()) + needs_install = (not NODE_MODULES.exists()) or (not has_required_dashboard_type_files()) if needs_install: if NODE_MODULES.exists(): - print("vis dependencies are incomplete; reinstalling with devDependencies...") - returncode = run_npm(npm, ["--prefix", str(VIS_DIR), "ci", "--include=dev"]) + print("dashboard dependencies are incomplete; reinstalling with devDependencies...") + returncode = run_npm(npm, ["--prefix", str(DASHBOARD_DIR), "ci", "--include=dev"]) if returncode != 0: return returncode - returncode = run_npm(npm, ["--prefix", str(VIS_DIR), "run", "build"]) + returncode = run_npm(npm, ["--prefix", str(DASHBOARD_DIR), "run", "build"]) if returncode != 0: return returncode if not DIST_DIR.exists(): - print("vis/dist not found after build. Check the vis build output.", file=sys.stderr) + print( + "dashboard/dist not found after build. Check the dashboard build output.", + file=sys.stderr, + ) return 1 if STATIC_DIR.exists(): @@ -98,7 +103,7 @@ def main() -> int: STATIC_DIR.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(DIST_DIR, STATIC_DIR) - print(f"Synced vis UI to {STATIC_DIR}") + print(f"Synced dashboard UI to {STATIC_DIR}") return 0 diff --git a/src/pythinker_code/cli/__init__.py b/src/pythinker_code/cli/__init__.py index 294fcf2d..38f43b65 100644 --- a/src/pythinker_code/cli/__init__.py +++ b/src/pythinker_code/cli/__init__.py @@ -36,11 +36,11 @@ def __init__(self, session_id: str | None = None): self.session_id = session_id -class SwitchToVis(Exception): - """Switch to vis (tracing visualizer) interface.""" +class SwitchToDashboard(Exception): + """Switch to dashboard (tracing visualizer) interface.""" - def __init__(self, session_id: str | None = None): - super().__init__("switch_to_vis") + def __init__(self, session_id: str | None = None) -> None: + super().__init__("switch_to_dashboard") self.session_id = session_id @@ -1042,7 +1042,7 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple except SwitchToWeb: preserve_background_tasks = True raise - except SwitchToVis: + except SwitchToDashboard: preserve_background_tasks = True raise finally: @@ -1134,10 +1134,10 @@ def _mark_last(metadata: Metadata) -> None: await asyncio.to_thread(mutate_metadata, _mark_last) async def _reload_loop(session_id: str | None) -> tuple[str | None, int]: - """Run the main loop, handling Reload/SwitchToWeb/SwitchToVis. + """Run the main loop, handling Reload/SwitchToWeb/SwitchToDashboard. Returns: - (switch_target, exit_code) where switch_target is "web", "vis", + (switch_target, exit_code) where switch_target is "web", "dashboard", or None if the session ended normally. """ last_session: Session | None = None @@ -1182,19 +1182,19 @@ async def _reload_loop(session_id: str | None) -> tuple[str | None, int]: if session is not None: await _post_run(session, ExitCode.SUCCESS) return "web", ExitCode.SUCCESS - except SwitchToVis as e: + except SwitchToDashboard as e: if _latest_created_session is not None: _latest_created_session.release_ownership() if e.session_id is not None: session = await Session.find(work_dir, e.session_id) if session is not None: await _post_run(session, ExitCode.SUCCESS) - return "vis", ExitCode.SUCCESS + return "dashboard", ExitCode.SUCCESS assert last_session is not None await _post_run(last_session, exit_code) last_session.release_ownership() return None, exit_code - except (SwitchToWeb, SwitchToVis): + except (SwitchToWeb, SwitchToDashboard): # Currently handled inside the loop (return), but re-raise explicitly # so the generic except below never treats them as unexpected errors. raise @@ -1316,7 +1316,7 @@ def _restore_term_and_exit(signum: int, frame: object) -> None: "Run with --debug for full traceback, or run pythinker export to share diagnostics." ) raise typer.Exit(code=1) from exc - if switch_target in ("web", "vis"): + if switch_target in ("web", "dashboard"): from pythinker_code.utils.logging import restore_stderr restore_stderr() @@ -1336,9 +1336,9 @@ def _restore_term_and_exit(signum: int, frame: object) -> None: run_web_server(open_browser=True) else: - from pythinker_code.vis.app import run_vis_server + from pythinker_code.dashboard.app import run_dashboard_server - run_vis_server(open_browser=True) + run_dashboard_server(open_browser=True) elif exit_code != ExitCode.SUCCESS: raise typer.Exit(code=exit_code) diff --git a/src/pythinker_code/cli/_lazy_group.py b/src/pythinker_code/cli/_lazy_group.py index 87f1a1c5..6cd9768b 100644 --- a/src/pythinker_code/cli/_lazy_group.py +++ b/src/pythinker_code/cli/_lazy_group.py @@ -45,7 +45,11 @@ class LazySubcommandGroup(typer.core.TyperGroup): "cli", "Check for and install Pythinker CLI updates.", ), - "vis": ("pythinker_code.cli.vis", "cli", "Run Pythinker Agent Tracing Visualizer."), + "dashboard": ( + "pythinker_code.cli.dashboard", + "cli", + "Run Pythinker Agent Tracing Visualizer.", + ), "web": ("pythinker_code.cli.web", "cli", "Run Pythinker CLI web interface."), } lazy_command_order: tuple[str, ...] = ( @@ -59,7 +63,7 @@ class LazySubcommandGroup(typer.core.TyperGroup): "security-scan", "debug", "update", - "vis", + "dashboard", "web", ) diff --git a/src/pythinker_code/cli/vis.py b/src/pythinker_code/cli/dashboard.py similarity index 81% rename from src/pythinker_code/cli/vis.py rename to src/pythinker_code/cli/dashboard.py index 32363d9f..e022f2f4 100644 --- a/src/pythinker_code/cli/vis.py +++ b/src/pythinker_code/cli/dashboard.py @@ -1,4 +1,4 @@ -"""Vis command for Pythinker Agent Tracing Visualizer.""" +"""Dashboard command for Pythinker Agent Tracing Visualizer.""" from typing import Annotated @@ -11,7 +11,7 @@ @cli.callback(invoke_without_command=True) -def vis( +def dashboard( _ctx: typer.Context, host: Annotated[ str | None, @@ -28,7 +28,7 @@ def vis( reload: Annotated[bool, typer.Option("--reload", help="Enable auto-reload")] = False, ): """Launch the agent tracing visualizer.""" - from pythinker_code.vis.app import run_vis_server + from pythinker_code.dashboard.app import run_dashboard_server # Determine bind address (same logic as pythinker web) if host: @@ -38,4 +38,4 @@ def vis( else: bind_host = "127.0.0.1" - run_vis_server(host=bind_host, port=port, open_browser=open_browser, reload=reload) + run_dashboard_server(host=bind_host, port=port, open_browser=open_browser, reload=reload) diff --git a/src/pythinker_code/vis/__init__.py b/src/pythinker_code/dashboard/__init__.py similarity index 100% rename from src/pythinker_code/vis/__init__.py rename to src/pythinker_code/dashboard/__init__.py diff --git a/src/pythinker_code/dashboard/api/__init__.py b/src/pythinker_code/dashboard/api/__init__.py new file mode 100644 index 00000000..d620b368 --- /dev/null +++ b/src/pythinker_code/dashboard/api/__init__.py @@ -0,0 +1,5 @@ +from pythinker_code.dashboard.api.sessions import router as sessions_router +from pythinker_code.dashboard.api.statistics import router as statistics_router +from pythinker_code.dashboard.api.system import router as system_router + +__all__ = ["sessions_router", "statistics_router", "system_router"] diff --git a/src/pythinker_code/vis/api/sessions.py b/src/pythinker_code/dashboard/api/sessions.py similarity index 98% rename from src/pythinker_code/vis/api/sessions.py rename to src/pythinker_code/dashboard/api/sessions.py index 3996f342..76a8ef12 100644 --- a/src/pythinker_code/vis/api/sessions.py +++ b/src/pythinker_code/dashboard/api/sessions.py @@ -1,4 +1,4 @@ -"""Vis API for reading session tracing data.""" +"""Dashboard API for reading session tracing data.""" from __future__ import annotations @@ -21,7 +21,7 @@ from pythinker_code.share import get_share_dir from pythinker_code.wire.file import WireFileMetadata, parse_wire_file_line -router = APIRouter(prefix="/api/vis", tags=["vis"]) +router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) logger = logging.getLogger(__name__) @@ -74,6 +74,7 @@ def get_work_dir_for_hash(hash_dir_name: str) -> str | None: try: metadata = load_metadata() except Exception: + logger.debug("Failed to load metadata for hash %s", hash_dir_name, exc_info=True) return None from hashlib import md5 @@ -124,7 +125,7 @@ def _extract_title_from_wire(wire_path: Path, max_bytes: int = 8192) -> tuple[st if bytes_read > max_bytes: break except Exception: - pass + logger.debug("Failed to extract title from %s", wire_path, exc_info=True) return title, turn_count diff --git a/src/pythinker_code/vis/api/statistics.py b/src/pythinker_code/dashboard/api/statistics.py similarity index 94% rename from src/pythinker_code/vis/api/statistics.py rename to src/pythinker_code/dashboard/api/statistics.py index 3f7c792c..6b68193d 100644 --- a/src/pythinker_code/vis/api/statistics.py +++ b/src/pythinker_code/dashboard/api/statistics.py @@ -1,7 +1,8 @@ -"""Vis API for aggregate statistics across all sessions.""" +"""Dashboard API for aggregate statistics across all sessions.""" from __future__ import annotations +import logging import time from collections import defaultdict from datetime import UTC, datetime, timedelta @@ -9,11 +10,12 @@ from fastapi import APIRouter +from pythinker_code.dashboard.api.sessions import collect_events, get_work_dir_for_hash from pythinker_code.share import get_share_dir -from pythinker_code.vis.api.sessions import collect_events, get_work_dir_for_hash from pythinker_code.wire.file import WireFileMetadata, parse_wire_file_line -router = APIRouter(prefix="/api/vis", tags=["vis"]) +router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) +logger = logging.getLogger(__name__) # Simple in-memory cache: (result, timestamp) @@ -142,6 +144,12 @@ def get_statistics() -> dict[str, Any]: ) session_output_tokens += int(tu.get("output", 0)) except Exception: + logger.warning( + "Skipping session %s from statistics: failed to read %s", + session_dir.name, + wire_path, + exc_info=True, + ) continue total_turns += session_turns diff --git a/src/pythinker_code/vis/api/system.py b/src/pythinker_code/dashboard/api/system.py similarity index 79% rename from src/pythinker_code/vis/api/system.py rename to src/pythinker_code/dashboard/api/system.py index b2d64742..4cd532e1 100644 --- a/src/pythinker_code/vis/api/system.py +++ b/src/pythinker_code/dashboard/api/system.py @@ -1,4 +1,4 @@ -"""Vis API for server capabilities and metadata.""" +"""Dashboard API for server capabilities and metadata.""" from __future__ import annotations @@ -7,7 +7,7 @@ from fastapi import APIRouter, Request -router = APIRouter(prefix="/api/vis", tags=["vis"]) +router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) @router.get("/capabilities") diff --git a/src/pythinker_code/vis/app.py b/src/pythinker_code/dashboard/app.py similarity index 80% rename from src/pythinker_code/vis/app.py rename to src/pythinker_code/dashboard/app.py index ad8bf2b9..d8da334d 100644 --- a/src/pythinker_code/vis/app.py +++ b/src/pythinker_code/dashboard/app.py @@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +from pythinker_code.dashboard.api import sessions_router, statistics_router, system_router from pythinker_code.utils.server import ( PYTHINKER_BANNER_ART, find_available_port, @@ -22,7 +23,6 @@ missing_ui_page, print_banner, ) -from pythinker_code.vis.api import sessions_router, statistics_router, system_router from pythinker_code.web.api.open_in import router as open_in_router from pythinker_code.web.auth import AuthMiddleware, normalize_allowed_origins @@ -30,9 +30,9 @@ GZIP_MINIMUM_SIZE = 1024 GZIP_COMPRESSION_LEVEL = 6 DEFAULT_PORT = 5495 -_ENV_RESTRICT_OPEN_IN = "PYTHINKER_VIS_RESTRICT_OPEN_IN" -_ENV_SESSION_TOKEN = "PYTHINKER_VIS_SESSION_TOKEN" -_ENV_ALLOWED_ORIGINS = "PYTHINKER_VIS_ALLOWED_ORIGINS" +_ENV_RESTRICT_OPEN_IN = "PYTHINKER_DASHBOARD_RESTRICT_OPEN_IN" +_ENV_SESSION_TOKEN = "PYTHINKER_DASHBOARD_SESSION_TOKEN" +_ENV_ALLOWED_ORIGINS = "PYTHINKER_DASHBOARD_ALLOWED_ORIGINS" def create_app() -> FastAPI: @@ -84,29 +84,41 @@ def create_app() -> FastAPI: async def health_probe() -> dict[str, Any]: # pyright: ignore[reportUnusedFunction] return {"status": "ok"} - # Gate on index.html, not just the directory: a build that skipped the vis - # bundle would otherwise answer "/" with a bare 404 instead of a hint. + # Gate on index.html, not just the directory: a build that skipped the + # dashboard bundle would otherwise answer "/" with a bare 404 instead of a hint. if (STATIC_DIR / "index.html").is_file(): application.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") else: @application.get("/", include_in_schema=False) - async def missing_vis_ui() -> HTMLResponse: # pyright: ignore[reportUnusedFunction] + async def missing_dashboard_ui() -> HTMLResponse: # pyright: ignore[reportUnusedFunction] return HTMLResponse( status_code=503, - content=missing_ui_page("pythinker_code/vis/static/index.html", "make build-vis"), + content=missing_ui_page( + "pythinker_code/dashboard/static/index.html", "make build-dashboard" + ), ) return application -def run_vis_server( +def loopback_browser_host(host: str) -> str: + """Map a wildcard bind address to a reachable loopback host for the browser. + + A server bound to ``0.0.0.0`` or ``::`` listens on every interface, but those + are not valid origins to open in a browser (and are absent from the allowed + origins list), so fall back to ``localhost``. + """ + return "localhost" if host in {"0.0.0.0", "::"} else host + + +def run_dashboard_server( host: str = "127.0.0.1", port: int = DEFAULT_PORT, reload: bool = False, open_browser: bool = True, ) -> None: - """Run the visualizer web server.""" + """Run the dashboard web server.""" import os import threading import webbrowser @@ -140,11 +152,13 @@ def run_vis_server( origin_hosts.append(host) elif host == "0.0.0.0": origin_hosts.extend(get_network_addresses()) + else: # host == "::" + origin_hosts.extend(["::1", "::"]) allowed_origins = [format_url(addr, actual_port) for addr in dict.fromkeys(origin_hosts)] os.environ[_ENV_ALLOWED_ORIGINS] = ",".join(allowed_origins) - # Browser should open localhost - browser_host = "localhost" if host == "0.0.0.0" else host + # Browser should open a reachable loopback host, not the wildcard bind address + browser_host = loopback_browser_host(host) browser_url = f"{format_url(browser_host, actual_port)}/?token={quote(session_token)}" banner_lines = [ @@ -194,7 +208,7 @@ def open_browser_after_delay() -> None: thread.start() uvicorn.run( - "pythinker_code.vis.app:create_app", + "pythinker_code.dashboard.app:create_app", factory=True, host=host, port=actual_port, @@ -204,4 +218,4 @@ def open_browser_after_delay() -> None: ) -__all__ = ["create_app", "run_vis_server"] +__all__ = ["create_app", "run_dashboard_server"] diff --git a/src/pythinker_code/ui/shell/__init__.py b/src/pythinker_code/ui/shell/__init__.py index 59e597a2..e0140239 100644 --- a/src/pythinker_code/ui/shell/__init__.py +++ b/src/pythinker_code/ui/shell/__init__.py @@ -1286,7 +1286,7 @@ def _handler(): remove_sigint() async def _run_slash_command(self, command_call: SlashCommandCall) -> None: - from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb + from pythinker_code.cli import Reload, SwitchToDashboard, SwitchToWeb from pythinker_code.telemetry import track available_command = self._find_available_slash_command(command_call.name) @@ -1318,7 +1318,7 @@ async def _run_slash_command(self, command_call: SlashCommandCall) -> None: ret = command.func(self, command_call.args) if isinstance(ret, Awaitable): await ret - except (Reload, SwitchToWeb, SwitchToVis): + except (Reload, SwitchToWeb, SwitchToDashboard): # just propagate raise except (asyncio.CancelledError, KeyboardInterrupt): @@ -1337,7 +1337,7 @@ async def _run_slash_command_during_task(self, command_call: SlashCommandCall) - change is already saved, so report that it applies later instead of letting the exception escape the fire-and-forget task. """ - from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb + from pythinker_code.cli import Reload, SwitchToDashboard, SwitchToWeb _t = _get_tui_tokens() try: @@ -1347,7 +1347,7 @@ async def _run_slash_command_during_task(self, command_call: SlashCommandCall) - f"[{_t.warning}]Settings saved — restart pythinker after the " f"current task to apply them.[/]" ) - except (SwitchToWeb, SwitchToVis): + except (SwitchToWeb, SwitchToDashboard): console.print( f"[{_t.warning}]Mode switches are unavailable while a task is in progress.[/]" ) diff --git a/src/pythinker_code/ui/shell/slash.py b/src/pythinker_code/ui/shell/slash.py index ed19b885..2d2f4876 100644 --- a/src/pythinker_code/ui/shell/slash.py +++ b/src/pythinker_code/ui/shell/slash.py @@ -10,7 +10,7 @@ from rich.markup import escape from pythinker_code.auth.platforms import get_platform_name_for_provider, refresh_managed_models -from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb +from pythinker_code.cli import Reload, SwitchToDashboard, SwitchToWeb from pythinker_code.config import StatusLineConfig, load_config, save_config from pythinker_code.exception import ConfigError from pythinker_code.session import Session @@ -1999,15 +1999,15 @@ def web(app: Shell, args: str): raise SwitchToWeb(session_id=session_id) -@registry.command -def reports(app: Shell, args: str) -> NoReturn: +@registry.command(aliases=["dashboard"]) +def reports(app: Shell, _args: str) -> NoReturn: """Open Pythinker session reports (Agent Tracing Visualizer) in browser""" from pythinker_code.telemetry import track track("reports_opened") soul = ensure_pythinker_soul(app) session_id = soul.runtime.session.id if soul else None - raise SwitchToVis(session_id=session_id) + raise SwitchToDashboard(session_id=session_id) @registry.command(name="memory", aliases=["mem"]) diff --git a/src/pythinker_code/utils/pyinstaller.py b/src/pythinker_code/utils/pyinstaller.py index c52a8719..29a80705 100644 --- a/src/pythinker_code/utils/pyinstaller.py +++ b/src/pythinker_code/utils/pyinstaller.py @@ -6,15 +6,15 @@ from pythinker_code.cli._lazy_group import LazySubcommandGroup -# The web/vis frontends are gitignored build artifacts synced into the package -# by scripts/build_web.py and scripts/build_vis.py. collect_data_files() +# The web/dashboard frontends are gitignored build artifacts synced into the package +# by scripts/build_web.py and scripts/build_dashboard.py. collect_data_files() # silently collects nothing for missing globs, which froze builds whose web UI # answered "/" with a 404 (Windows/Linux native installers). -_REQUIRED_UI_ASSETS = ("web/static/index.html", "vis/static/index.html") +_REQUIRED_UI_ASSETS = ("web/static/index.html", "dashboard/static/index.html") def require_ui_assets(package_root: Path | None = None) -> None: - """Abort the freeze when the web/vis UI bundles haven't been built.""" + """Abort the freeze when the web/dashboard UI bundles haven't been built.""" if package_root is None: package_root = Path(__file__).resolve().parents[1] missing = [rel for rel in _REQUIRED_UI_ASSETS if not (package_root / rel).is_file()] @@ -22,7 +22,7 @@ def require_ui_assets(package_root: Path | None = None) -> None: raise SystemExit( "PyInstaller build aborted: UI bundles missing from pythinker_code " f"({', '.join(missing)}). They are gitignored build artifacts; run " - "`make build-web build-vis` before freezing, or the packaged web UI " + "`make build-web build-dashboard` before freezing, or the packaged web UI " "will 404 on '/'." ) @@ -55,7 +55,7 @@ def require_ui_assets(package_root: Path | None = None) -> None: "skills/**", "tools/**/*.md", "web/static/**", - "vis/static/**", + "dashboard/static/**", "CHANGELOG.md", ], excludes=[ diff --git a/src/pythinker_code/utils/server.py b/src/pythinker_code/utils/server.py index d61cdd88..1397835a 100644 --- a/src/pythinker_code/utils/server.py +++ b/src/pythinker_code/utils/server.py @@ -1,4 +1,4 @@ -"""Shared utilities for pythinker vis and pythinker web server startup.""" +"""Shared utilities for pythinker dashboard and pythinker web server startup.""" from __future__ import annotations @@ -9,7 +9,7 @@ from pythinker_code.ui.terminal_capabilities import ascii_glyphs_enabled -# Shared "PYTHINKER" wordmark used by the web and vis startup banners. +# Shared "PYTHINKER" wordmark used by the web and dashboard startup banners. PYTHINKER_BANNER_ART = [ "
██████╗ ██╗ ██╗████████╗██╗ ██╗██╗███╗ ██╗██╗ ██╗███████╗██████╗ ", "
██╔══██╗╚██╗ ██╔╝╚══██╔══╝██║ ██║██║████╗ ██║██║ ██╔╝██╔════╝██╔══██╗", diff --git a/src/pythinker_code/utils/subprocess_env.py b/src/pythinker_code/utils/subprocess_env.py index 6ba69bc9..8ca45fed 100644 --- a/src/pythinker_code/utils/subprocess_env.py +++ b/src/pythinker_code/utils/subprocess_env.py @@ -24,7 +24,7 @@ # arbitrary child tools, plugins, hooks, git helpers, or shell commands. _INTERNAL_SESSION_TOKEN_VARS = { "PYTHINKER_WEB_SESSION_TOKEN", - "PYTHINKER_VIS_SESSION_TOKEN", + "PYTHINKER_DASHBOARD_SESSION_TOKEN", } diff --git a/src/pythinker_code/vis/api/__init__.py b/src/pythinker_code/vis/api/__init__.py deleted file mode 100644 index db314713..00000000 --- a/src/pythinker_code/vis/api/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from pythinker_code.vis.api.sessions import router as sessions_router -from pythinker_code.vis.api.statistics import router as statistics_router -from pythinker_code.vis.api.system import router as system_router - -__all__ = ["sessions_router", "statistics_router", "system_router"] diff --git a/tests/core/test_cli_reload.py b/tests/core/test_cli_reload.py index c75492f5..3fade478 100644 --- a/tests/core/test_cli_reload.py +++ b/tests/core/test_cli_reload.py @@ -10,7 +10,7 @@ _load_mcp_configs_from_cli_inputs, _yaml_files_with_misplaced_mcp_servers, ) -from pythinker_code.cli.vis import cli as vis_cli +from pythinker_code.cli.dashboard import cli as dashboard_cli from pythinker_code.cli.web import cli as web_cli @@ -101,7 +101,7 @@ def test_load_mcp_configs_ignores_misplaced_yaml( def test_web_dash_h_shows_help_not_host() -> None: - """-h on web/vis CLIs must show help (exit 0), not demand a host argument (exit 2).""" + """-h on web/dashboard CLIs must show help (exit 0), not demand a host argument (exit 2).""" runner = CliRunner() # web: -h shows help @@ -113,13 +113,13 @@ def test_web_dash_h_shows_help_not_host() -> None: result_h = runner.invoke(web_cli, ["-H", "1.2.3.4", "--help"], color=False) assert result_h.exit_code == 0 - # vis: -h shows help - result_vis = runner.invoke(vis_cli, ["-h"], color=False) - assert result_vis.exit_code == 0, ( - f"vis -h exit_code={result_vis.exit_code!r}, output={result_vis.output!r}" + # dashboard: -h shows help + result_dashboard = runner.invoke(dashboard_cli, ["-h"], color=False) + assert result_dashboard.exit_code == 0, ( + f"dashboard -h exit_code={result_dashboard.exit_code!r}, output={result_dashboard.output!r}" ) - assert "Usage" in result_vis.output + assert "Usage" in result_dashboard.output - # vis: -H sets host without error - result_vis_h = runner.invoke(vis_cli, ["-H", "1.2.3.4", "--help"], color=False) - assert result_vis_h.exit_code == 0 + # dashboard: -H sets host without error + result_dashboard_h = runner.invoke(dashboard_cli, ["-H", "1.2.3.4", "--help"], color=False) + assert result_dashboard_h.exit_code == 0 diff --git a/tests/core/test_startup_imports.py b/tests/core/test_startup_imports.py index 6fd3f97f..e6f2bd1d 100644 --- a/tests/core/test_startup_imports.py +++ b/tests/core/test_startup_imports.py @@ -79,7 +79,7 @@ def test_root_help_lists_lazy_subcommands_without_importing_them() -> None: "pythinker_code.cli.info", "pythinker_code.cli.export", "pythinker_code.cli.mcp", - "pythinker_code.cli.vis", + "pythinker_code.cli.dashboard", "pythinker_code.cli.web", ] for name in lazy_modules: @@ -91,7 +91,7 @@ def test_root_help_lists_lazy_subcommands_without_importing_them() -> None: result = CliRunner().invoke(cli, ["--help"]) assert result.exit_code == 0, result.output -for name in ("info", "export", "mcp", "vis", "web"): +for name in ("info", "export", "mcp", "dashboard", "web"): assert name in result.output assert all(name not in sys.modules for name in lazy_modules) print("ok") @@ -110,7 +110,7 @@ def test_info_subcommand_loads_on_demand() -> None: "pythinker_code.cli.info", "pythinker_code.cli.export", "pythinker_code.cli.mcp", - "pythinker_code.cli.vis", + "pythinker_code.cli.dashboard", "pythinker_code.cli.web", ] for name in lazy_modules: @@ -124,7 +124,7 @@ def test_info_subcommand_loads_on_demand() -> None: assert "pythinker_code.cli.info" in sys.modules assert "pythinker_code.cli.export" not in sys.modules assert "pythinker_code.cli.mcp" not in sys.modules -assert "pythinker_code.cli.vis" not in sys.modules +assert "pythinker_code.cli.dashboard" not in sys.modules assert "pythinker_code.cli.web" not in sys.modules print("ok") """ diff --git a/tests/core/test_wire_file_compat.py b/tests/core/test_wire_file_compat.py index 39a4be33..8c1c81a4 100644 --- a/tests/core/test_wire_file_compat.py +++ b/tests/core/test_wire_file_compat.py @@ -3,7 +3,7 @@ Pins the version-detection behaviour of `WireFile`: files written before the metadata header existed (legacy, headerless) must still be readable and must report `WIRE_PROTOCOL_LEGACY_VERSION`. A regression here would silently break -old `wire.jsonl` files for every frontend (Shell, Web, Vis, ACP). +old `wire.jsonl` files for every frontend (Shell, Web, Dashboard, ACP). """ from __future__ import annotations diff --git a/tests/vis/test_app.py b/tests/dashboard/test_app.py similarity index 67% rename from tests/vis/test_app.py rename to tests/dashboard/test_app.py index a975a101..d150245c 100644 --- a/tests/vis/test_app.py +++ b/tests/dashboard/test_app.py @@ -4,14 +4,30 @@ import zipfile from pathlib import Path +import pytest from fastapi.testclient import TestClient +from pythinker_code.dashboard.api import system as dashboard_system_api +from pythinker_code.dashboard.app import create_app, loopback_browser_host from pythinker_code.metadata import Metadata, WorkDirMeta, save_metadata -from pythinker_code.vis.api import system as vis_system_api -from pythinker_code.vis.app import create_app -def test_vis_sessions_include_session_dir( +@pytest.mark.parametrize( + ("host", "expected"), + [ + ("0.0.0.0", "localhost"), + ("::", "localhost"), + ("127.0.0.1", "127.0.0.1"), + ("192.168.1.5", "192.168.1.5"), + ], +) +def test_loopback_browser_host_maps_wildcard_to_localhost(host: str, expected: str) -> None: + # Wildcard bind addresses are not valid browser origins, so they must fall + # back to localhost (which is the only loopback entry in allowed_origins). + assert loopback_browser_host(host) == expected + + +def test_dashboard_sessions_include_session_dir( monkeypatch, tmp_path: Path, ) -> None: @@ -27,7 +43,7 @@ def test_vis_sessions_include_session_dir( (session_dir / "context.jsonl").write_text("{}\n", encoding="utf-8") with TestClient(create_app()) as client: - response = client.get("/api/vis/sessions") + response = client.get("/api/dashboard/sessions") assert response.status_code == 200 payload = response.json() @@ -37,7 +53,7 @@ def test_vis_sessions_include_session_dir( assert payload[0]["work_dir"] == str(work_dir) -def test_vis_app_mounts_open_in_route() -> None: +def test_dashboard_app_mounts_open_in_route() -> None: with TestClient(create_app()) as client: response = client.post( "/api/open-in", @@ -47,11 +63,11 @@ def test_vis_app_mounts_open_in_route() -> None: assert response.status_code == 400 -def test_vis_capabilities_report_open_in_support(monkeypatch) -> None: - monkeypatch.setattr(vis_system_api.sys, "platform", "linux") +def test_dashboard_capabilities_report_open_in_support(monkeypatch) -> None: + monkeypatch.setattr(dashboard_system_api.sys, "platform", "linux") with TestClient(create_app()) as client: - response = client.get("/api/vis/capabilities") + response = client.get("/api/dashboard/capabilities") assert response.status_code == 200 assert response.json() == {"open_in_supported": False} @@ -65,13 +81,13 @@ def _zip_bytes(entries: dict[str, str]) -> bytes: return buf.getvalue() -def test_vis_import_rejects_zip_slip_entries(monkeypatch, tmp_path: Path) -> None: +def test_dashboard_import_rejects_zip_slip_entries(monkeypatch, tmp_path: Path) -> None: monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) payload = _zip_bytes({"wire.jsonl": "{}\n", "../evil.txt": "owned"}) with TestClient(create_app()) as client: response = client.post( - "/api/vis/sessions/import", + "/api/dashboard/sessions/import", files={"file": ("session.zip", payload, "application/zip")}, ) @@ -80,13 +96,13 @@ def test_vis_import_rejects_zip_slip_entries(monkeypatch, tmp_path: Path) -> Non assert not (tmp_path / "evil.txt").exists() -def test_vis_import_rejects_dot_member(monkeypatch, tmp_path: Path) -> None: +def test_dashboard_import_rejects_dot_member(monkeypatch, tmp_path: Path) -> None: monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) payload = _zip_bytes({"wire.jsonl": "{}\n", ".": ""}) with TestClient(create_app()) as client: response = client.post( - "/api/vis/sessions/import", + "/api/dashboard/sessions/import", files={"file": ("session.zip", payload, "application/zip")}, ) @@ -96,13 +112,13 @@ def test_vis_import_rejects_dot_member(monkeypatch, tmp_path: Path) -> None: assert not imported_root.exists() or list(imported_root.iterdir()) == [] -def test_vis_import_accepts_safe_zip(monkeypatch, tmp_path: Path) -> None: +def test_dashboard_import_accepts_safe_zip(monkeypatch, tmp_path: Path) -> None: monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) payload = _zip_bytes({"session/wire.jsonl": "{}\n"}) with TestClient(create_app()) as client: response = client.post( - "/api/vis/sessions/import", + "/api/dashboard/sessions/import", files={"file": ("session.zip", payload, "application/zip")}, ) diff --git a/tests/tools/test_memory_routing_guard.py b/tests/tools/test_memory_routing_guard.py index 146a5d62..5c8d6ac7 100644 --- a/tests/tools/test_memory_routing_guard.py +++ b/tests/tools/test_memory_routing_guard.py @@ -58,7 +58,7 @@ def test_file_reference_trips(content: str) -> None: [ "Project uses pytest with xdist", "User prefers concise answers", - "The web and vis frontends are gitignored", + "The web and dashboard frontends are gitignored", "Sessions are stored under the share directory", ], ) diff --git a/tests/ui_and_conv/test_shell_switch_slash.py b/tests/ui_and_conv/test_shell_switch_slash.py index aaa4eea7..ec0f6142 100644 --- a/tests/ui_and_conv/test_shell_switch_slash.py +++ b/tests/ui_and_conv/test_shell_switch_slash.py @@ -14,7 +14,7 @@ import pytest -from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb +from pythinker_code.cli import Reload, SwitchToDashboard, SwitchToWeb from pythinker_code.ui.shell.slash import ShellSlashCmdFunc, shell_mode_registry from pythinker_code.ui.shell.slash import registry as shell_slash_registry from pythinker_code.utils.slashcmd import SlashCommand @@ -103,8 +103,8 @@ async def test_session_id_none_without_pythinker_soul(self) -> None: assert exc_info.value.session_id is None - async def test_does_not_raise_switch_to_vis(self) -> None: - """/web must raise SwitchToWeb, not SwitchToVis.""" + async def test_does_not_raise_switch_to_dashboard(self) -> None: + """/web must raise SwitchToWeb, not SwitchToDashboard.""" shell = _mock_shell_with_soul() cmd = shell_slash_registry.find_command("web") @@ -143,15 +143,15 @@ def test_not_in_soul_registry(self) -> None: class TestReportsCommandBehavior: - """Verify /reports raises SwitchToVis with the current session ID.""" + """Verify /reports raises SwitchToDashboard with the current session ID.""" - async def test_raises_switch_to_vis(self) -> None: + async def test_raises_switch_to_dashboard(self) -> None: shell = _mock_shell_with_soul("my-session-123") cmd = shell_slash_registry.find_command("reports") assert cmd is not None - with pytest.raises(SwitchToVis) as exc_info: + with pytest.raises(SwitchToDashboard) as exc_info: await _invoke_slash_command(cmd, shell) assert exc_info.value.session_id == "my-session-123" @@ -162,7 +162,7 @@ async def test_carries_session_id(self) -> None: cmd = shell_slash_registry.find_command("reports") assert cmd is not None - with pytest.raises(SwitchToVis) as exc_info: + with pytest.raises(SwitchToDashboard) as exc_info: await _invoke_slash_command(cmd, shell) assert exc_info.value.session_id == "abc-def" @@ -175,58 +175,58 @@ async def test_session_id_none_without_pythinker_soul(self) -> None: cmd = shell_slash_registry.find_command("reports") assert cmd is not None - with pytest.raises(SwitchToVis) as exc_info: + with pytest.raises(SwitchToDashboard) as exc_info: await _invoke_slash_command(cmd, shell) assert exc_info.value.session_id is None async def test_does_not_raise_switch_to_web(self) -> None: - """/reports must raise SwitchToVis, not SwitchToWeb.""" + """/reports must raise SwitchToDashboard, not SwitchToWeb.""" shell = _mock_shell_with_soul() cmd = shell_slash_registry.find_command("reports") assert cmd is not None - with pytest.raises(SwitchToVis): + with pytest.raises(SwitchToDashboard): await _invoke_slash_command(cmd, shell) # --------------------------------------------------------------------------- -# SwitchToWeb / SwitchToVis — exception properties +# SwitchToWeb / SwitchToDashboard — exception properties # --------------------------------------------------------------------------- class TestSwitchExceptionProperties: - """Verify SwitchToWeb and SwitchToVis have consistent interfaces.""" + """Verify SwitchToWeb and SwitchToDashboard have consistent interfaces.""" def test_both_are_exceptions(self) -> None: assert issubclass(SwitchToWeb, Exception) - assert issubclass(SwitchToVis, Exception) + assert issubclass(SwitchToDashboard, Exception) def test_independent_hierarchies(self) -> None: """Neither should be a subclass of the other.""" - assert not issubclass(SwitchToVis, SwitchToWeb) - assert not issubclass(SwitchToWeb, SwitchToVis) + assert not issubclass(SwitchToDashboard, SwitchToWeb) + assert not issubclass(SwitchToWeb, SwitchToDashboard) def test_str_representations(self) -> None: assert str(SwitchToWeb()) == "switch_to_web" - assert str(SwitchToVis()) == "switch_to_vis" + assert str(SwitchToDashboard()) == "switch_to_dashboard" def test_default_session_id_is_none(self) -> None: assert SwitchToWeb().session_id is None - assert SwitchToVis().session_id is None + assert SwitchToDashboard().session_id is None def test_accepts_session_id(self) -> None: assert SwitchToWeb(session_id="x").session_id == "x" - assert SwitchToVis(session_id="x").session_id == "x" + assert SwitchToDashboard(session_id="x").session_id == "x" def test_matching_interface(self) -> None: """Both exceptions must expose the same ``session_id`` attribute.""" web = SwitchToWeb(session_id="s") - vis = SwitchToVis(session_id="s") + dashboard = SwitchToDashboard(session_id="s") assert hasattr(web, "session_id") - assert hasattr(vis, "session_id") - assert web.session_id == vis.session_id + assert hasattr(dashboard, "session_id") + assert web.session_id == dashboard.session_id # --------------------------------------------------------------------------- @@ -239,7 +239,7 @@ class TestShellExceptionPropagation: The shell's slash command runner has a try/except that catches generic exceptions and prints them as errors. Reload, SwitchToWeb, and - SwitchToVis must be in the propagation whitelist so they reach the + SwitchToDashboard must be in the propagation whitelist so they reach the outer _reload_loop handler instead of being swallowed. """ @@ -248,7 +248,7 @@ async def test_propagates_through_shell_runner(self) -> None: for exc in ( Reload(session_id="t"), SwitchToWeb(session_id="t"), - SwitchToVis(session_id="t"), + SwitchToDashboard(session_id="t"), ): raised = False @@ -260,7 +260,7 @@ def thrower(*args: Any, _exc: Exception = exc, **kwargs: Any) -> None: # Mimic the exact try/except structure from Shell._run_slash_command try: cmd.func(Mock(), "") - except (Reload, SwitchToWeb, SwitchToVis): + except (Reload, SwitchToWeb, SwitchToDashboard): raised = True except (asyncio.CancelledError, KeyboardInterrupt): pass @@ -280,27 +280,27 @@ class TestWebAndReportsCoexistence: def test_both_registered(self) -> None: web_cmd = shell_slash_registry.find_command("web") - vis_cmd = shell_slash_registry.find_command("reports") + dashboard_cmd = shell_slash_registry.find_command("reports") assert web_cmd is not None - assert vis_cmd is not None - assert web_cmd.name != vis_cmd.name + assert dashboard_cmd is not None + assert web_cmd.name != dashboard_cmd.name async def test_same_shell_different_exceptions(self) -> None: - """Given the same shell, /web raises SwitchToWeb and /reports raises SwitchToVis.""" + """Given the same shell, /web raises SwitchToWeb and /reports raises SwitchToDashboard.""" shell = _mock_shell_with_soul("shared-session") web_cmd = shell_slash_registry.find_command("web") - vis_cmd = shell_slash_registry.find_command("reports") + dashboard_cmd = shell_slash_registry.find_command("reports") assert web_cmd is not None - assert vis_cmd is not None + assert dashboard_cmd is not None with pytest.raises(SwitchToWeb) as web_exc: await _invoke_slash_command(web_cmd, shell) - with pytest.raises(SwitchToVis) as vis_exc: - await _invoke_slash_command(vis_cmd, shell) + with pytest.raises(SwitchToDashboard) as dashboard_exc: + await _invoke_slash_command(dashboard_cmd, shell) - assert web_exc.value.session_id == vis_exc.value.session_id == "shared-session" + assert web_exc.value.session_id == dashboard_exc.value.session_id == "shared-session" # --------------------------------------------------------------------------- diff --git a/tests/utils/test_pyinstaller_utils.py b/tests/utils/test_pyinstaller_utils.py index e07f409b..b62d1567 100644 --- a/tests/utils/test_pyinstaller_utils.py +++ b/tests/utils/test_pyinstaller_utils.py @@ -60,7 +60,7 @@ def test_pyinstaller_datas(): (p, d) for p, d in datas if "web/static" not in d - and "vis/static" not in d + and "dashboard/static" not in d and d != "justext/stoplists" and not any(d == di or d.startswith(di + "/") for di in dist_info_dirs) ] @@ -284,6 +284,7 @@ def test_pyinstaller_hiddenimports(): assert project_entries == snapshot( [ "pythinker_code.cli._lazy_group", + "pythinker_code.cli.dashboard", "pythinker_code.cli.debug", "pythinker_code.cli.export", "pythinker_code.cli.info", @@ -294,7 +295,6 @@ def test_pyinstaller_hiddenimports(): "pythinker_code.cli.security_scan", "pythinker_code.cli.skill", "pythinker_code.cli.update", - "pythinker_code.cli.vis", "pythinker_code.cli.web", "pythinker_code.tools", "pythinker_code.tools.agent", @@ -354,7 +354,7 @@ def test_pyinstaller_hiddenimports_include_lazy_cli_subcommands(): def test_require_ui_assets_accepts_built_tree(tmp_path: Path) -> None: from pythinker_code.utils.pyinstaller import require_ui_assets - for rel in ("web/static/index.html", "vis/static/index.html"): + for rel in ("web/static/index.html", "dashboard/static/index.html"): target = tmp_path / rel target.parent.mkdir(parents=True) target.write_text("") @@ -363,7 +363,7 @@ def test_require_ui_assets_accepts_built_tree(tmp_path: Path) -> None: def test_require_ui_assets_rejects_unbuilt_tree(tmp_path: Path) -> None: - """A freeze without the gitignored web/vis bundles ships a web UI that + """A freeze without the gitignored web/dashboard bundles ships a web UI that 404s on "/"; the guard must abort the build with an actionable message.""" from pythinker_code.utils.pyinstaller import require_ui_assets diff --git a/tests/utils/test_subprocess_env.py b/tests/utils/test_subprocess_env.py index 58ab14a8..30e0478b 100644 --- a/tests/utils/test_subprocess_env.py +++ b/tests/utils/test_subprocess_env.py @@ -23,11 +23,11 @@ def test_clean_env_removes_internal_session_tokens(): base_env={ "PATH": "/usr/bin", "PYTHINKER_WEB_SESSION_TOKEN": "web-secret", - "PYTHINKER_VIS_SESSION_TOKEN": "vis-secret", + "PYTHINKER_DASHBOARD_SESSION_TOKEN": "dashboard-secret", } ) assert "PYTHINKER_WEB_SESSION_TOKEN" not in env - assert "PYTHINKER_VIS_SESSION_TOKEN" not in env + assert "PYTHINKER_DASHBOARD_SESSION_TOKEN" not in env # --- get_noninteractive_env --- diff --git a/tests/web/test_web_ui_assets.py b/tests/web/test_web_ui_assets.py index b6046d80..f45a73d9 100644 --- a/tests/web/test_web_ui_assets.py +++ b/tests/web/test_web_ui_assets.py @@ -1,7 +1,7 @@ """When the bundled frontend is missing, "/" must explain why, not 404. Regression guard for the native installers that froze without building the -gitignored web/vis bundles: the served app answered ``GET /?token=...`` with a +gitignored web/dashboard bundles: the served app answered ``GET /?token=...`` with a bare 404 (see windows-installer.yml / linux-installer.yml web build steps). """ @@ -28,12 +28,15 @@ def test_web_root_explains_missing_assets(monkeypatch: pytest.MonkeyPatch, tmp_p assert "pythinker_code/web/static/index.html" in resp.text -def test_vis_root_explains_missing_assets(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - import pythinker_code.vis.app as vis_app +def test_dashboard_root_explains_missing_assets( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + import pythinker_code.dashboard.app as dashboard_app - monkeypatch.setattr(vis_app, "STATIC_DIR", tmp_path) - with TestClient(vis_app.create_app()) as client: + monkeypatch.setattr(dashboard_app, "STATIC_DIR", tmp_path) + with TestClient(dashboard_app.create_app()) as client: resp = client.get("/") assert resp.status_code == 503 - assert "make build-vis" in resp.text + assert "make build-dashboard" in resp.text + assert "pythinker_code/dashboard/static/index.html" in resp.text