From 365f350c4b3a6deb9e80cb5651072060638956f7 Mon Sep 17 00:00:00 2001 From: elkaix Date: Sun, 23 Aug 2026 14:38:51 -0400 Subject: [PATCH 1/4] chore: drop unreferenced maintenance scripts These three scripts were run by hand only. No workflow, package script, or source file referenced them, so removing them changes no build, test, or release behaviour. --- scripts/upstream-sync/README.md | 129 ----------------- scripts/upstream-sync/check-managed.mjs | 130 ------------------ scripts/upstream-sync/rebrand.mjs | 175 ------------------------ 3 files changed, 434 deletions(-) delete mode 100644 scripts/upstream-sync/README.md delete mode 100644 scripts/upstream-sync/check-managed.mjs delete mode 100644 scripts/upstream-sync/rebrand.mjs diff --git a/scripts/upstream-sync/README.md b/scripts/upstream-sync/README.md deleted file mode 100644 index 1d4b11c6f..000000000 --- a/scripts/upstream-sync/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Upstream sync - -This workflow imports Kimi Code releases into Pythinker Code without changing the Pythinker product identity. `blackbox/refrence` is the read-only Kimi upstream checkout; Pythinker development and releases stay in this repository. - -## Brand boundary - -- Never merge `blackbox/refrence/main` directly into Pythinker `main`. -- Import a full upstream tree through `rebrand.mjs`, commit that tree on `vendor/upstream`, then merge `vendor/upstream` into a fresh sync branch. -- Keep Pythinker names, package scopes, URLs, logos, managed-service policy, and retained fork features. -- Keep deliberate Kimi/Moonshot provider data: `api.moonshot.*`, `api.kimi.com`, `platform.kimi.*`, `kimi-k*` models, `kimi-for-coding`, `moonshot-*` provider IDs, and `MOONSHOT_API_KEY`. -- Merge the pull request with a merge commit. Squash or rebase removes the vendor ancestry that future three-way merges need. - -## Import a release - -Run each step from the Pythinker repository root unless the command uses `git -C`. - -1. Fetch Kimi and select the release commit. - - ```sh - git -C blackbox/refrence fetch origin - git -C blackbox/refrence log --oneline --decorate origin/main - git -C blackbox/refrence show --stat - ``` - -2. Export and rebrand the selected tree. Both output locations must be disposable task-local directories because `rebrand.mjs` replaces its output directory. - - ```sh - export_root="$(mktemp -d "${TMPDIR:-/tmp}/pythinker-upstream.XXXXXX")" - rebrand_root="$(mktemp -d "${TMPDIR:-/tmp}/pythinker-rebrand.XXXXXX")" - export_dir="$export_root/tree" - rebrand_dir="$rebrand_root/tree" - mkdir "$export_dir" - git -C blackbox/refrence archive | tar -x -C "$export_dir" - node scripts/upstream-sync/rebrand.mjs "$export_dir" "$rebrand_dir" - ``` - -3. Replace the `vendor/upstream` snapshot in a dedicated worktree and commit it. - - ```sh - worktree_root="$(mktemp -d "${TMPDIR:-/tmp}/pythinker-vendor.XXXXXX")" - vendor_worktree="$worktree_root/worktree" - git worktree add "$vendor_worktree" vendor/upstream - test "$(git -C "$vendor_worktree" branch --show-current)" = "vendor/upstream" - rsync --archive --delete --exclude .git "$rebrand_dir/" "$vendor_worktree/" - git -C "$vendor_worktree" status --short - git -C "$vendor_worktree" add -A - git -C "$vendor_worktree" commit -m "vendor: rebrand Kimi Code " - ``` - -4. Create a fresh sync branch from current Pythinker `main`, then merge the vendor snapshot. Keep rerere enabled so recorded conflict resolutions replay. - - ```sh - git switch main - git pull --ff-only - git switch -c sync/upstream- - git config rerere.enabled true - git merge --no-ff vendor/upstream - ``` - -5. Resolve only genuine new conflicts. Retain Pythinker branding and fork features; do not restore upstream managed-account behavior. - -## Post-merge checks - -Run these checks in order. - -1. Find silently deleted files and compare them with the vendor tree. Restore a path that exists in `vendor/upstream` unless it is an intentional Pythinker deletion. - - ```sh - git diff --name-only ORIG_HEAD..HEAD --diff-filter=D - git ls-tree -r --name-only vendor/upstream - ``` - -2. Require zero camel-case rename residue. - - ```sh - rg 'dynamic_workflow[A-Z]' --glob '!scripts/upstream-sync/README.md' - ``` - -3. Audit every brand match. Only the provider values listed in [Brand boundary](#brand-boundary) can remain. - - ```sh - rg -in 'kimi|moonshot' - ``` - -4. Require English-only source. Pythinker does not ship Chinese localization. - - ```sh - if rg --pcre2 '\p{Unified_Ideograph}' apps packages plugins scripts \ - --glob '*.{ts,tsx,vue,js,mjs,cjs,sh,ps1}' \ - --glob '!**/dist*/**'; then - echo 'Literal Han ideographs found in source.' >&2 - exit 1 - fi - ``` - -5. Verify that upstream did not restore the removed managed service. - - ```sh - node scripts/upstream-sync/check-managed.mjs - ``` - -6. Run the full gates separately and record each exit status. - - ```sh - pnpm run build - pnpm run typecheck - pnpm run lint - pnpm run sherif - pnpm test - pnpm -C apps/vscode run typecheck - pnpm -C apps/vscode test - nix build .#pythinker-code - node scripts/check-nix-workspace.mjs - node scripts/upstream-sync/check-managed.mjs - ``` - -If `pnpm-lock.yaml` changed, update the `pnpmDeps` hash in `flake.nix` and rerun the Nix build. Treat a full-suite failure as red until the exact failing file passes in isolation and the load-related difference is documented. - -## Pull request - -Use a Conventional Commit title, fill the pull request template, run `gen-changesets`, and run the local review CLI when the diff exceeds the hosted reviewer limit. Merge only after required checks pass and every review conversation is resolved. Use a merge commit so `main` retains `vendor/upstream` ancestry. - -## Known traps - -- 3-way merges silently delete files our history removed but upstream didn't touch — always diff vendor file list vs worktree after a merge. -- Git rename detection pairs upstream `vis` with `dashboard` — gone now, but watch for similar pairings. -- Prose rename rules corrupt identifiers; the camel regex (`swarm(?=[A-Z])`) must stay ahead of the snake fallback in rebrand.mjs. Vendor snapshots built before the fix still carry `dynamic_workflowX` residue — sweep after merging. -- Merged package.json/vite.config lose our test tooling (jsdom, @vue/test-utils, test block) — re-check after every web merge. -- Vendor tree carries managed-service code back in on every sync — the D5 strip must be rerere-recorded deletions, plus the grep gate as a backstop. diff --git a/scripts/upstream-sync/check-managed.mjs b/scripts/upstream-sync/check-managed.mjs deleted file mode 100644 index dba14be8f..000000000 --- a/scripts/upstream-sync/check-managed.mjs +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env node -/** - * D5 gate: the managed-service strip must survive every upstream sync. - * Each check pins one choke point removed from the vendor tree; a future - * 3-way merge that reintroduces managed sign-in or managed fallbacks fails - * here instead of shipping silently. Run from the repo root. - */ -import { existsSync, readFileSync } from 'node:fs'; -import { execSync } from 'node:child_process'; - -const failures = []; - -function check(label, ok) { - if (!ok) failures.push(label); -} - -function read(path) { - return readFileSync(path, 'utf8'); -} - -const platformSelector = read('apps/pythinker-code/src/tui/components/dialogs/platform-selector.ts'); -check( - 'TUI login menu must not offer the managed pythinker-code OAuth entry', - !platformSelector.includes("'pythinker-code'"), -); - -const oauthRoutes = read('packages/agent-gateway/src/routes/oauth.ts'); -check( - 'agent-gateway must not expose /oauth/usage', - !oauthRoutes.includes('/oauth/usage'), -); -check( - 'agent-gateway must not expose /oauth/userinfo', - !oauthRoutes.includes('/oauth/userinfo'), -); -check( - 'agent-gateway /oauth/login must reject the managed provider (PROVIDER_OAUTH_MANAGED guard)', - oauthRoutes.includes('PROVIDER_OAUTH_MANAGED'), -); - -check( - 'web app must not ship the managed LoginDialog', - !existsSync('apps/pythinker-web/src/components/dialogs/LoginDialog.vue'), -); - -const identity = read('packages/oauth/src/identity.ts'); -const defaultHeadersBody = identity.slice(identity.indexOf('function createPythinkerDefaultHeaders')); -check( - 'createPythinkerDefaultHeaders must not attach X-Msh device headers', - !defaultHeadersBody.slice(0, defaultHeadersBody.indexOf('}')).includes('X-Msh'), -); - -for (const file of [ - 'packages/agent-core-v2/src/app/web/webService.ts', - 'packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts', -]) { - check( - `${file} must not fall back to the managed OAuth provider`, - !read(file).includes('fromManagedOAuth'), - ); -} - -const trackedFiles = execSync('git ls-files -z').toString().split('\0').filter(Boolean); -const kimiHostPattern = /\b(?:[a-z0-9-]+\.)*kimi\.com\b/gi; - -// kaos→pyaos rename guard: the only tracked files allowed to mention the old -// name are the deprecated-alias surfaces (config `executor: 'kaos'`, SDK -// `{kaos, persistenceKaos}` session params) and their tests. pnpm-lock.yaml is -// excluded for its unrelated base64 `...kAOs...` integrity hash. Changesets and -// the CHANGELOG.md files generated from them are excluded for the same reason: -// they are a release record of the rename, and rewriting a published entry -// would falsify history rather than remove residue. -const kaosAliasAllowlist = new Set([ - 'packages/agent-core/src/config/schema.ts', - 'packages/agent-core-v2/src/mcpCore/config-schema.ts', - 'packages/klient/src/contract/mcp.ts', - 'packages/node-sdk/src/types.ts', - 'packages/node-sdk/src/pythinker-harness.ts', - 'packages/agent-core/test/config/configs.test.ts', - 'packages/agent-core-v2/test/mcpCore/client-stdio.test.ts', - 'packages/klient/test/contract.test.ts', - 'packages/node-sdk/test/create-session-transport.test.ts', -]); -const kaosPattern = /kaos/i; - -for (const file of trackedFiles) { - if (file.startsWith('scripts/upstream-sync/') || file.startsWith('blackbox/')) continue; - - let contents; - try { - contents = readFileSync(file); - } catch { - continue; - } - if (contents.length > 2 * 1024 * 1024 || contents.includes(0)) continue; - - let text; - try { - text = new TextDecoder('utf-8', { fatal: true }).decode(contents); - } catch { - continue; - } - - if ( - file !== 'pnpm-lock.yaml' && - !file.startsWith('.changeset/') && - !file.endsWith('CHANGELOG.md') && - !kaosAliasAllowlist.has(file) && - kaosPattern.test(text) - ) { - failures.push(`${file} — legacy 'kaos' residue (rename to pyaos, or extend the alias allowlist)`); - } - - for (const [index, line] of text.split(/\r?\n/).entries()) { - for (const match of line.matchAll(kimiHostPattern)) { - const host = match[0].toLowerCase(); - if (host === 'api.kimi.com' || host === 'auth.kimi.com' || host.startsWith('platform.kimi.')) { - continue; - } - failures.push(`${file}:${index + 1} — ${match[0]}`); - } - } -} - -if (failures.length > 0) { - console.error('check-managed: FAILED'); - for (const f of failures) console.error(` - ${f}`); - process.exit(1); -} -console.log('check-managed: OK'); diff --git a/scripts/upstream-sync/rebrand.mjs b/scripts/upstream-sync/rebrand.mjs deleted file mode 100644 index ac1871413..000000000 --- a/scripts/upstream-sync/rebrand.mjs +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env node -/** - * Deterministic, idempotent rebrand transform for continuous upstream porting: - * MoonshotAI/kimi-code tree -> Pythinker naming. - * - * Usage: node scripts/upstream-sync/rebrand.mjs - * - * Product identity is renamed; the Kimi/Moonshot MODEL PROVIDER is kept: - * provider platform ids (moonshot-cn/moonshot-ai), api.moonshot.* URLs, - * platform.kimi.* consoles, kimi-k* model names, "Kimi Platform" labels, - * kimi-for-coding, and moonshot-v1 model ids. - * Managed-service stripping is deliberately NOT done here — that lives on the - * merge side (tasks/todo.md D5) so this script stays mechanical. - */ -import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; -import { join, relative } from 'node:path'; - -const BINARY_EXT = new Set([ - '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.icns', '.pdf', '.zip', - '.gz', '.tar', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.mp4', '.mov', - '.wasm', '.node', '.dylib', '.so', '.dll', '.exe', '.wav', '.mp3', -]); - -// Org/scope/docs renames that must run BEFORE protection: they contain -// substrings (moonshot-ai, moonshotai) that the protection pass keeps for -// the model provider. -const RENAME_FIRST = [ - ['@moonshot-ai/kimi-code', '@pymodel/pythinker-code'], - ['@moonshot-ai', '@pymodel'], - ['moonshotai.github.io/kimi-code', 'code.pythinker.com/pythinker-code'], - ['github.com/MoonshotAI/kimi-code', 'github.com/PyModel/pythinker-code'], - ['MoonshotAI/kimi-code', 'PyModel/pythinker-code'], - ['moonshot-ai/kimi-code', 'pymodel/pythinker-code'], -]; - -// Substrings that must survive rebranding (the Kimi/Moonshot MODEL PROVIDER). -// Each is swapped to a placeholder before the rename rules run, and restored -// afterwards. -const PROTECT = [ - 'api.moonshot.cn', - 'api.moonshot.ai', - 'api.kimi.com', - 'API.KIMI.COM', - "'kimi-k'", - 'platform.kimi.com', - 'platform.kimi.ai', - 'www.kimi.com', - 'kimi.com', - 'kimi.ai', - 'Kimi Platform', - 'Kimi Open Platform', - 'kimi-for-coding', - 'cloudbase-kimi.zip', - 'kimi-k1', - 'kimi-k2', - 'kimi-k3', - 'kimi-k4', - 'kimi-latest', - 'kimi-thinking', - 'Kimi K1', - 'Kimi K2', - 'Kimi K3', - 'moonshot-cn', - 'moonshot-ai', - 'moonshot-v1', - 'moonshotai/', // huggingface-style org paths for model weights - 'MOONSHOT_API_KEY', -]; - -// Ordered rename rules (longest / most specific first). Plain string pairs; -// applied globally to both file contents and paths. -const RENAME = [ - // company + managed web services (search/fetch providers) — PyModel only - ['Moonshot AI', 'PyModel'], - ['MoonshotAI', 'PyModel'], - ['Moonshot', 'PyModel'], - ['MOONSHOT', 'PYMODEL'], - ['moonshot', 'pymodel'], - // swarm feature was renamed to dynamic workflow in pythinker - ['SWARM_MODE', 'DYNAMIC_WORKFLOW_MODE'], - ['SWARM_', 'DYNAMIC_WORKFLOW_'], - ['SWARM', 'DYNAMIC_WORKFLOW'], - ['swarm_mode', 'dynamic_workflow_mode'], - ['swarmMode', 'dynamicWorkflowMode'], - ['swarm_index', 'dynamic_workflow_index'], - ['swarmIndex', 'dynamicWorkflowIndex'], - ['SwarmMode', 'DynamicWorkflowMode'], - ['swarm-', 'dynamic-workflow-'], - ['Swarm', 'DynamicWorkflow'], - ['swarms', 'dynamicWorkflows'], - ['swarm', 'dynamic_workflow'], - // kaos OS-abstraction layer was renamed to pyaos in pythinker - ['KAOS_', 'PYAOS_'], - ['KAOS', 'PYAOS'], - ['Kaos', 'Pyaos'], - ['kaos', 'pyaos'], - // product identity - ['kimi-code', 'pythinker-code'], - ['KimiCode', 'PythinkerCode'], - ['kimiCode', 'pythinkerCode'], - ['KIMI_CODE', 'PYTHINKER_CODE'], - ['Kimi Code', 'Pythinker Code'], - ['Kimi-Code', 'Pythinker-Code'], - ['KIMI_', 'PYTHINKER_'], - ['KIMI', 'PYTHINKER'], - ['Kimi', 'Pythinker'], - ['kimi', 'pythinker'], -]; - -function transformText(text) { - let out = text; - for (const [from, to] of RENAME_FIRST) out = out.split(from).join(to); - PROTECT.forEach((s, i) => { - out = out.split(s).join(`\u0000P${i}\u0000`); - }); - // camelCase continuations (swarmItem, swarmMembers, ...) must stay camel: - // handle them before the bare snake_case fallback rule below. - out = out.replaceAll(/swarm(?=[A-Z])/g, 'dynamicWorkflow'); - for (const [from, to] of RENAME) out = out.split(from).join(to); - PROTECT.forEach((s, i) => { - out = out.split(`\u0000P${i}\u0000`).join(s); - }); - return out; -} - -function transformPath(p) { - // Paths never contain the protected URL/label strings above except plugin - // product dirs, which PROTECT handles via the same placeholder mechanism. - return transformText(p); -} - -function isBinary(path, buf) { - const dot = path.lastIndexOf('.'); - if (dot !== -1 && BINARY_EXT.has(path.slice(dot).toLowerCase())) return true; - return buf.subarray(0, 8192).includes(0); -} - -function walk(dir, base, files) { - for (const entry of readdirSync(dir)) { - if (entry === '.git') continue; - const full = join(dir, entry); - const st = statSync(full); - if (st.isDirectory()) walk(full, base, files); - else files.push(relative(base, full)); - } - return files; -} - -const [src, out] = process.argv.slice(2); -if (!src || !out) { - console.error('usage: rebrand.mjs '); - process.exit(1); -} -rmSync(out, { recursive: true, force: true }); -mkdirSync(out, { recursive: true }); - -let renamedPaths = 0; -let changedFiles = 0; -const files = walk(src, src, []); -for (const rel of files) { - const target = transformPath(rel); - if (target !== rel) renamedPaths++; - const dest = join(out, target); - mkdirSync(join(dest, '..'), { recursive: true }); - const buf = readFileSync(join(src, rel)); - if (isBinary(rel, buf)) { - cpSync(join(src, rel), dest); - continue; - } - const text = buf.toString('utf8'); - const next = transformText(text); - if (next !== text) changedFiles++; - writeFileSync(dest, next); -} -console.log(`rebranded ${files.length} files (${renamedPaths} paths renamed, ${changedFiles} contents changed)`); From 9e1ce7dac318c0e0ec023099660af121aec27fb1 Mon Sep 17 00:00:00 2001 From: elkaix Date: Sun, 23 Aug 2026 14:50:43 -0400 Subject: [PATCH 2/4] docs: describe web behaviour on its own terms Several comments and one AGENTS.md bullet explained a behaviour by pointing at an external artefact instead of describing it, which left the surrounding code unexplained to anyone without that artefact. Say what each piece does. --- apps/pythinker-web/AGENTS.md | 1 - apps/pythinker-web/src/App.vue | 2 +- .../src/api/daemon/agentEventProjector.ts | 6 ++---- apps/pythinker-web/src/components/Sidebar.vue | 2 +- .../src/components/chat/ActivityRun.vue | 2 +- .../src/components/chat/ApprovalCard.vue | 2 +- .../src/components/chat/ChatPane.vue | 4 ++-- .../src/components/chat/DiffView.vue | 4 ++-- .../src/components/chat/HighlightedCode.vue | 2 +- .../src/components/chat/Markdown.vue | 8 ++++---- .../src/components/chat/MentionMenu.vue | 5 ++--- .../src/components/chat/SlashMenu.vue | 2 +- .../chat/tool-calls/waitForToolParse.ts | 2 +- .../components/settings/SecondaryModelPicker.vue | 6 +++--- .../src/components/settings/SettingsDialog.vue | 2 +- .../src/composables/useBodyScrollLock.ts | 6 +++--- .../src/composables/useMenuScrollbar.ts | 3 +-- .../src/i18n/locales/en/conversation.ts | 2 +- apps/pythinker-web/src/lib/inlineMath.ts | 5 ++--- .../src/lib/markdownFrontmatter.test.ts | 2 +- .../pythinker-web/src/lib/markdownFrontmatter.ts | 4 ++-- apps/pythinker-web/src/lib/matchHighlight.ts | 16 ++++++++-------- apps/pythinker-web/src/types.ts | 2 +- .../test/app-shell-contracts.test.ts | 2 +- .../test/chat-turn-rendering.test.ts | 2 +- apps/pythinker-web/test/continue-turn.test.ts | 2 +- apps/pythinker-web/test/model-display.test.ts | 2 +- .../agent-core-v2/src/_base/text/encoding.ts | 2 +- .../agent/turnRecovery/outputTokenRecovery.ts | 2 +- 29 files changed, 48 insertions(+), 54 deletions(-) diff --git a/apps/pythinker-web/AGENTS.md b/apps/pythinker-web/AGENTS.md index 7d9d2571e..5e81a70ba 100644 --- a/apps/pythinker-web/AGENTS.md +++ b/apps/pythinker-web/AGENTS.md @@ -61,7 +61,6 @@ Debugging against agent-gateway instances: start one from the repo root with `pn - **Theming:** the root element carries `data-color-scheme` (`light` | `dark` | `system`); react to it through `useIsDark()`, not by reading the DOM directly. - Keep the Vite **dev** proxy and **`preview`** proxy in sync — both are defined in `vite.config.ts` (shared `apiProxyOptions`). - The shared proxy strips the browser `Origin` header on forwarded requests: `changeOrigin` rewrites `Host` to the server but leaves `Origin` pointing at the Vite origin, and agent-gateway's WS upgrade path rejects that mismatch with 403. An Origin-less request is treated as a non-browser client. If you add another proxied path, route it through the same options. -- **Upstream design parity:** the upstream removed its web UI source (2026-08-05); its current design exists only compiled in the reference checkout's `dist-web` bundle (`blackbox/refrence/apps/*/dist-web`, primary checkout only — gitignored). The last full upstream web source, already rebranded, is vendor commit `f12110e95` (`vendor/upstream`). When porting design, extract from the compiled bundle (component render fns + scoped CSS by `data-v` hash) rather than guessing. The rebranded 0.37.1 bundle is also available in-repo from any worktree: `git show 144c7c7d8:apps/pythinker-code/dist-web/assets/index-BdL5hCoZ.js` (main chunk, ~118k lines beautified; locate components by `__name:` markers) plus `index-CiiPSBw1.css`. - **Dynamic Workflow has manual entry points:** the composer "+" menu row, the active-chip dismiss, and the mobile settings sheet switch all route through `client.toggleDynamicWorkflowMode()` (guarded by `test/app-shell-contracts.test.ts`); the active chip still renders from server-set session state, and `/workflow` is a daemon-routed command (`test/daemon-contracts.test.ts`). ## Maintaining this file diff --git a/apps/pythinker-web/src/App.vue b/apps/pythinker-web/src/App.vue index 1eaa3b496..17e49125f 100644 --- a/apps/pythinker-web/src/App.vue +++ b/apps/pythinker-web/src/App.vue @@ -879,7 +879,7 @@ async function handleAddWorkspacePaths(paths: string[]): Promise { // Generate a session title via the daemon's managed chat_title tool. The // daemon persists the title itself (the list refreshes via the WS event); the // result streams back into the rename input through the callback. Unavailable -// generation surfaces as an info toast, mirroring the reference UI. +// generation surfaces as an info toast. async function handleGenerateSessionTitle( sessionId: string, onTitle: (title: string | null) => void, diff --git a/apps/pythinker-web/src/api/daemon/agentEventProjector.ts b/apps/pythinker-web/src/api/daemon/agentEventProjector.ts index 5e9f675a0..fad1d13a1 100644 --- a/apps/pythinker-web/src/api/daemon/agentEventProjector.ts +++ b/apps/pythinker-web/src/api/daemon/agentEventProjector.ts @@ -6,10 +6,8 @@ // protocol events). This projector translates them into the same AppEvent union // that the existing reducer (eventReducer.ts) consumes. // -// Ported from the daemon-side reference implementation: -// apps/pythinker-daemon/src/session/event-projector.ts -// apps/pythinker-daemon/src/session/message-log.ts -// apps/pythinker-daemon/src/session/usage-tracker.ts +// It owns three concerns the server keeps separate: event projection, the +// message log, and usage tracking. // // Usage: // const projector = createAgentProjector(); diff --git a/apps/pythinker-web/src/components/Sidebar.vue b/apps/pythinker-web/src/components/Sidebar.vue index ce81081dd..0783d39e0 100644 --- a/apps/pythinker-web/src/components/Sidebar.vue +++ b/apps/pythinker-web/src/components/Sidebar.vue @@ -1207,7 +1207,7 @@ onBeforeUnmount(() => { .side.macos-desktop .ch-collapse { -webkit-app-region: no-drag; } -/* Compact brand lockup on macOS, matching the reference proportions. */ +/* Compact brand lockup on macOS. */ .side.macos-desktop .ch-logo { height: 24px; width: 24px; diff --git a/apps/pythinker-web/src/components/chat/ActivityRun.vue b/apps/pythinker-web/src/components/chat/ActivityRun.vue index cf4a27848..7d7e0f415 100644 --- a/apps/pythinker-web/src/components/chat/ActivityRun.vue +++ b/apps/pythinker-web/src/components/chat/ActivityRun.vue @@ -303,7 +303,7 @@ function toggle(): void { /** Only the run's last thinking item streams (the daemon streams one tail * item at a time; a settled thinking block never animates). The durationMs - * guard mirrors the reference `_()`: a thinking whose step ended keeps its + * guard: a thinking whose step ended keeps its * frozen "Thinking · Ns" label instead of shimmering forever while the run * stays open. */ function isItemStreaming(item: RunItem): boolean { diff --git a/apps/pythinker-web/src/components/chat/ApprovalCard.vue b/apps/pythinker-web/src/components/chat/ApprovalCard.vue index 46cab6f57..331534c31 100644 --- a/apps/pythinker-web/src/components/chat/ApprovalCard.vue +++ b/apps/pythinker-web/src/components/chat/ApprovalCard.vue @@ -138,7 +138,7 @@ const feedbackRef = ref(null); // --------------------------------------------------------------------------- // Feedback textarea autosize: grows with its content up to 40% of the visual // viewport height, then scrolls. Re-measured on resize / font-scale change / -// width changes (mirrors the reference, which re-measures on the same cues). +// width changes (re-measured on the same cues). // --------------------------------------------------------------------------- const FEEDBACK_MAX_HEIGHT_RATIO = 0.4; diff --git a/apps/pythinker-web/src/components/chat/ChatPane.vue b/apps/pythinker-web/src/components/chat/ChatPane.vue index 300502fc6..165a8f13c 100644 --- a/apps/pythinker-web/src/components/chat/ChatPane.vue +++ b/apps/pythinker-web/src/components/chat/ChatPane.vue @@ -276,7 +276,7 @@ const emit = defineEmits<{ reorderQueue: [payload: { from: number; to: number }]; /** * Failed-turn recovery: submit a fixed "Continue" prompt (no attachments), - * mirroring the reference client's resume path. + * on the resume path. */ continueTurn: [text: string]; }>(); @@ -664,7 +664,7 @@ function runIsStreaming( } // Failed-turn recovery: submit a fixed "Continue" prompt with no attachments, -// matching the reference client (its ConversationPane submits +// matching the conversation pane, which submits // `conversation.turnFailedResumeText` through the ordinary send path). The // user's own last message is deliberately NOT re-sent: that would repeat its // instructions and any side effects. diff --git a/apps/pythinker-web/src/components/chat/DiffView.vue b/apps/pythinker-web/src/components/chat/DiffView.vue index dfb6c71c7..8fa5bdbfd 100644 --- a/apps/pythinker-web/src/components/chat/DiffView.vue +++ b/apps/pythinker-web/src/components/chat/DiffView.vue @@ -55,8 +55,8 @@ const emit = defineEmits<{ close: []; }>(); -// Status badge: single-letter glyph + CSS class (glyphs mirror the upstream -// file-status legend: '+' added/untracked, '−' deleted, '→' renamed) +// Status badge: single-letter glyph + CSS class +// ('+' added/untracked, '−' deleted, '→' renamed) type BadgeKind = 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'conflicted' | 'ignored' | 'clean' | 'unknown'; function badgeKind(s: string): BadgeKind { diff --git a/apps/pythinker-web/src/components/chat/HighlightedCode.vue b/apps/pythinker-web/src/components/chat/HighlightedCode.vue index 9dbb086db..05a5d7fcc 100644 --- a/apps/pythinker-web/src/components/chat/HighlightedCode.vue +++ b/apps/pythinker-web/src/components/chat/HighlightedCode.vue @@ -23,7 +23,7 @@ interface HighlightToken { type CodeToTokensLang = Parameters[1]['lang']; -// Extension → Shiki language id (mirrors the upstream map). +// Extension → Shiki language id. const EXT_LANG: Record = { ts: 'ts', tsx: 'tsx', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js', vue: 'vue', svelte: 'svelte', py: 'py', rb: 'rb', go: 'go', rs: 'rs', diff --git a/apps/pythinker-web/src/components/chat/Markdown.vue b/apps/pythinker-web/src/components/chat/Markdown.vue index ecb1b8071..c9fff9679 100644 --- a/apps/pythinker-web/src/components/chat/Markdown.vue +++ b/apps/pythinker-web/src/components/chat/Markdown.vue @@ -70,7 +70,7 @@ setKaTeXWorker(new katexWorkerModule.default()); setMermaidWorker(new mermaidWorkerModule.default()); // --------------------------------------------------------------------------- -// Inline `$…$` math — curated detector (ported from the reference web UI). +// Inline `$…$` math — curated detector. // // The stock `math` rule is too permissive for prose: prices, env vars and // shell paths (`$5`, `$PATH`, `$HOME/bin`, `US$100`) all look like math. The @@ -94,7 +94,7 @@ interface MathInlineState { // matcher is cheap to build per source but the rule runs once per `$`, so // cache it on the inline state for the lifetime of a parse run (same WeakMap -// pattern as the reference). +// pattern). const inlineMathCache = new WeakMap; lastEnd: number }>(); function inlineMathRule(state: MathInlineState, silent: boolean): boolean { @@ -467,7 +467,7 @@ function pillInfo(pill: HTMLElement): PillInfo { return { kind, name, path: pill.dataset.mentionPath ?? '' }; } -// Tokens read once from the stylesheet (with fallbacks), like the reference's +// Tokens read once from the stylesheet (with fallbacks), like the // `yg()` cache; `--duration-*` values are seconds here, so convert to ms. function cssVarMs(name: string, fallback: number): () => number { let cached: number | undefined; @@ -716,7 +716,7 @@ function onTipGlobalScroll(): void { // (see ChatPane.vue: `@container (min-width:760px)`); here we only create and // drive the chrome: the toggle button, the right-edge fade, the "at end" state // and the scroll-synced transforms. Tables outside `.a-msg .msg` get no -// toggle, matching the reference `a$e` gate. +// toggle. // --------------------------------------------------------------------------- const TABLE_WIDE_CLASS = 'md-table-wide'; diff --git a/apps/pythinker-web/src/components/chat/MentionMenu.vue b/apps/pythinker-web/src/components/chat/MentionMenu.vue index a4355c2e2..8288b1af4 100644 --- a/apps/pythinker-web/src/components/chat/MentionMenu.vue +++ b/apps/pythinker-web/src/components/chat/MentionMenu.vue @@ -1,9 +1,8 @@ + caller feeds them in, skill rows. Stale results dim, a custom scrollbar + replaces the native one, and the list fades at its scroll edges. --> Pythinker Code Web - - + +
diff --git a/apps/pythinker-web/src/api/daemon/eventReducer.ts b/apps/pythinker-web/src/api/daemon/eventReducer.ts index 907680f84..0da9efc2d 100644 --- a/apps/pythinker-web/src/api/daemon/eventReducer.ts +++ b/apps/pythinker-web/src/api/daemon/eventReducer.ts @@ -248,7 +248,7 @@ function appendToolOutputToMessages(messages: AppMessage[], toolCallId: string, } /** - * Settle open thinking parts in `content` (reference `cp`): a thinking part + * Settle open thinking parts in `content`: a thinking part * streams only while it is the current tail — the moment a later content part * starts (or the step/turn ends) its `durationMs` freezes, so the render layer * stops treating it as live. `beforeIndex` limits settling to parts before @@ -563,8 +563,8 @@ export function reduceAppEvent( if (m.id !== event.messageId) return m; // Carry per-part thinking timing across the wholesale content swap: the // projector's copy has none (timing is born here, on first delta), so - // map by index and keep the existing startedAt/durationMs (reference - // messageUpdated). Then settle open thinking parts: a pending update + // map by index and keep the existing startedAt/durationMs. Then + // settle open thinking parts: a pending update // (new tool-use slot) freezes every part before the new tail; a // completed/duration-stamped update freezes the tail too. const content = event.content.map((part, index) => { @@ -597,8 +597,8 @@ export function reduceAppEvent( const idx = event.contentIndex; // Track whether the slot pre-existed: the placeholder loop below pads // text slots, so a padded slot must be treated as a NEW part (settle - // the thinking parts before it), not a continuation (reference: the - // `c` flag in assistantDelta). + // the thinking parts before it), not a continuation — the `c` flag + // in assistantDelta marks it. const created = content.length <= idx; // Ensure the slot exists while (content.length <= idx) { @@ -610,7 +610,7 @@ export function reduceAppEvent( if (existing.type === 'text' && !created) { patched = { type: 'text', text: existing.text + event.delta.text }; } else { - // A fresh text part ends any thinking part before it (reference cp). + // A fresh text part ends any thinking part before it. settleOpenThinking(content, Date.now(), idx); patched = { type: 'text', text: event.delta.text }; } diff --git a/apps/pythinker-web/src/api/types.ts b/apps/pythinker-web/src/api/types.ts index 0f21fce6e..339110594 100644 --- a/apps/pythinker-web/src/api/types.ts +++ b/apps/pythinker-web/src/api/types.ts @@ -162,7 +162,7 @@ export type AppMessageContent = // `durationMs` settles the moment the next content part starts (or the // step/turn ends). A thinking part with `durationMs` set is never the live // streaming tail — the render layer relies on this to collapse settled - // thinking instead of shimmering a second "Thinking…" row (reference parity). + // thinking instead of shimmering a second "Thinking…" row. | { type: 'thinking'; thinking: string; signature?: string; startedAt?: string; durationMs?: number } | { type: 'unknown'; raw: unknown }; diff --git a/apps/pythinker-web/src/components/Sidebar.vue b/apps/pythinker-web/src/components/Sidebar.vue index 0783d39e0..f18ea5ec0 100644 --- a/apps/pythinker-web/src/components/Sidebar.vue +++ b/apps/pythinker-web/src/components/Sidebar.vue @@ -552,10 +552,9 @@ onBeforeUnmount(() => { // --------------------------------------------------------------------------- // Folder-drop to add a workspace: dragging an OS folder onto the column shows // the drop overlay and emits the resolved paths upward (App adds them via the -// existing addWorkspace flow). Reference parity — path resolution needs the -// desktop shell (Kimi's kimiDesktop.getPathForFile); the browser itself cannot -// read absolute paths, so the interaction only activates inside the desktop -// app. We extract via the legacy Electron `File.path` — on shells that expose +// existing addWorkspace flow). Path resolution needs the desktop shell: a +// browser cannot read an absolute path out of a drop, so the interaction only +// activates inside the desktop app. We extract via the legacy Electron `File.path` — on shells that expose // neither, the overlay still shows but the drop resolves no paths (no-op). // --------------------------------------------------------------------------- const dropDepth = ref(0); @@ -1196,7 +1195,7 @@ onBeforeUnmount(() => { } /* macOS desktop: the window uses a hidden title bar, so the traffic lights float over the top-left of the sidebar. The brand row sits BELOW them (its - own line, like the design-system reference): padding-top clears the lights, + own line, like the design-system spec): padding-top clears the lights, left padding returns to the normal sidebar gutter, and the whole strip — lights zone included — stays a window-drag area while the collapse button opts out so it remains clickable. */ diff --git a/apps/pythinker-web/src/components/chat/ActivityRun.vue b/apps/pythinker-web/src/components/chat/ActivityRun.vue index 7d7e0f415..130bed132 100644 --- a/apps/pythinker-web/src/components/chat/ActivityRun.vue +++ b/apps/pythinker-web/src/components/chat/ActivityRun.vue @@ -1,5 +1,5 @@ - - - @@ -69,14 +69,14 @@ function isOpenable(task: TaskItem): boolean { v-for="(task, index) in tasks" :key="task.id" class="sg-card" - :class="[`s-${referenceTask(task).state}`, { openable: isOpenable(task) }]" + :class="[`s-${extendedTask(task).state}`, { openable: isOpenable(task) }]" >