feat: add commit attribution with per-file AI contribution tracking#88
Open
BingqingLyu wants to merge 22 commits into
Open
feat: add commit attribution with per-file AI contribution tracking#88BingqingLyu wants to merge 22 commits into
BingqingLyu wants to merge 22 commits into
Conversation
…ia git notes Track character-level AI vs human contributions per file and store detailed attribution metadata as git notes (refs/notes/ai-attribution) after each successful git commit. This enables open-source AI disclosure and enterprise compliance audits without polluting commit messages.
…ted file exclusion
- Replace line-based diff with a prefix/suffix character-level algorithm
for precise contribution calculation (e.g. "Esc"→"esc" = 1 char, not whole line)
- Compute real AI vs human contribution percentages at commit time by analyzing
git diff --stat output: humanChars = max(0, diffSize - trackedAiChars)
- Add generated file exclusion (lock files, dist/, .min.js, .d.ts, etc.)
ported from an existing generatedFiles.ts
- Add file deletion tracking via recordDeletion()
- Update git notes payload format: {aiChars, humanChars, percent} per file
with real percentages instead of hardcoded 100%
… PR attribution Align with the full attribution feature set: - Surface tracking: read QWEN_CODE_ENTRYPOINT env var (cli/ide/api/sdk), include surfaceBreakdown in git notes payload - Prompt counting: incrementPromptCount() hooked into client.ts message loop, tracks promptCount/permissionPromptCount/escapeCount - Session persistence: toSnapshot()/restoreFromSnapshot() for serializing attribution state; ChatRecordingService.recordAttributionSnapshot() writes to session JSONL; client.ts restores on session resume - PR attribution: addAttributionToPR() in shell.ts detects `gh pr create` and appends "🤖 Generated with Qwen Code (N-shotted by Qwen-Coder)" - Session baseline: saves content hash on first AI edit of each file for precise human/AI contribution detection - generatePRAttribution() method for programmatic access
…iled commit counter preservation - Handle initial commit (no HEAD~1) by detecting parent with rev-parse and falling back to --root for first commit in repo - Exclude Cron-triggered messages from promptCount (not user-initiated) - Add commitSucceeded parameter to clearAttributions() so failed/disabled commits don't reset the prompts-since-last-commit counter - Add test for clearAttributions(false) behavior
- Normalize path.relative() to forward slashes for Windows compatibility - Use diff-tree --root for initial commits (git diff --root is invalid) - Replace String.replace() with indexOf+slice to avoid $& special patterns - Fix clearAttributions(false→true) when co-author disabled but commit succeeded - Use real newlines instead of literal \n in PR attribution text - Add surface fallback in restoreFromSnapshot for version compatibility - Fix single-quote regex to not assume bash supports \' escaping - Case-insensitive directory matching in generated file detection - Handle renamed file brace notation in parseDiffStat
# Conflicts: # packages/core/src/core/client.ts
# Conflicts: # packages/core/src/core/client.ts # packages/core/src/services/chatRecordingService.ts
…ool edits Previously, recordAttributionSnapshot() only ran at the start of UserQuery and Cron turns — before the tools for that turn had executed. A session that wrote a file in turn 1 and committed in turn 2 (across process boundaries via --resume) lost the tracked edit: the last persisted snapshot was the turn-1-start snapshot (empty fileStates), so on resume the attribution service restored empty state and no git notes were attached to the commit. Move the snapshot call out of the UserQuery/Cron conditional and run it on every non-Retry turn. ToolResult turns are scheduled right after tools execute, so their start-of-turn snapshot now captures any edits those tools made. Retry turns are skipped since the state is unchanged from the prior turn. Added unit tests asserting the snapshot fires for ToolResult/UserQuery turns and skips Retry turns. Verified end-to-end in a scratch repo: write-file in turn 1 (no commit) → exit → --resume → commit in turn 2 → git notes now contain the recorded file with correct aiChars and promptCount: 2.
Collapse the two back-to-back messageType !== Retry blocks in sendMessageStream into one, and refresh chatRecordingService's recordAttributionSnapshot doc comment to reflect that snapshots fire on every non-retry turn (not just after user prompts).
…#3559) params.pages !== undefined let "" fall through to parsePDFPageRange(''), which returns null and surfaced "Invalid pages parameter: ''" for every read_file call from models that default optional strings to "". Switch to a truthy check so "" behaves the same as an omitted field, and add a regression test. Fixes QwenLM#3558
…QwenLM#3540) * feat(session): auto-title sessions via fast model, add /rename --auto The /rename work in QwenLM#3093 generates kebab-case titles only when the user explicitly runs `/rename` with no args; until they do, the session picker shows the first user prompt (often truncated or misleading). This change adds a sentence-case auto-title that fires once per session after the first assistant turn, using the configured fast model. New service: `packages/core/src/services/sessionTitle.ts` — `tryGenerateSessionTitle(config, signal)` returns a discriminated outcome (`{ok: true, title, modelUsed}` | `{ok: false, reason}`) so callers can either handle failures generically or map reasons to actionable messages. Prompt shape: 3-7 words, sentence case, good/bad examples including a CJK row, JSON schema enforced via `baseLlmClient.generateJson`. `maxAttempts: 1` — titles are cosmetic metadata and shouldn't fight rate limits. Trigger point: `ChatRecordingService.maybeTriggerAutoTitle` runs after `recordAssistantTurn`. Fire-and-forget promise, guarded by: - `currentCustomTitle` — don't overwrite any existing title. - `autoTitleController` doubles as in-flight flag; a second turn while the first is still pending is a no-op. - `autoTitleAttempts` cap of 3 — the first assistant turn may be a pure tool-call with no user-visible text; retry for a handful of turns until a title lands. Cap bounds total waste. - `!config.isInteractive()` — headless CLI (`qwen -p`, CI) never auto- titles; spending fast-model tokens on a one-shot session is waste. - `autoTitleDisabledByEnv()` — `QWEN_DISABLE_AUTO_TITLE=1` opt-out. - `config.getFastModel()` falsy — skip entirely rather than falling back to the main model; auto-titling on main-model tokens is too expensive to be silent. Persistence: `CustomTitleRecordPayload` grows a `titleSource: 'auto' | 'manual'` field. Absent on pre-change records (treated as `undefined` → manual, safe default so a user's pre-upgrade `/rename` is never silently reclassified). `SessionPicker` renders `titleSource === 'auto'` titles in dim (secondary) color; manual stays full contrast. On resume, the persisted source is rehydrated into `currentTitleSource` — without this, finalize's re-append would rewrite an auto title as manual on every resume cycle. Cross-process manual-rename guard: when two CLI tabs target the same JSONL, in-memory state can diverge. Before writing an auto record, the IIFE re-reads the file via `sessionService.getSessionTitleInfo`. If a `/rename` from another process landed as manual, bail and sync local state — never clobber a deliberately-chosen manual title with a model guess. Cost is one 64KB tail read per successful generation. `finalize()` aborts the in-flight controller before re-appending the title record. Session switch / shutdown doesn't have to wait on a slow fast-model call. New user-facing command: `/rename --auto` regenerates via the same generator — explicit user trigger, overwrites whatever's there (manual or auto) because the user asked. Errors route through `autoFailureMessage(reason)` so `empty_history`, `model_error`, `aborted`, etc. each get actionable guidance rather than a generic "could not generate". `/rename -- --literal-name` is the sentinel for titles that start with `--`; unknown `--flag` tokens error with a hint pointing at the sentinel. Existing `/rename <name>` and bare `/rename` (kebab-case via existing path) are unchanged, except the kebab path now prefers fast model when available and runs its output through `stripTerminalControlSequences` (same ANSI/OSC-8 hardening as the sentence-case path). New shared util: `packages/core/src/utils/terminalSafe.ts` — `stripTerminalControlSequences(s)` strips OSC (\x1b]...\x07|\x1b\\), CSI (\x1b[...[a-zA-Z]), SS2/SS3 leaders, and C0/C1/DEL as a backstop. A model-returned `\x1b[2J` or OSC-8 hyperlink escape would otherwise execute on every SessionPicker render; both sentence-case and kebab paths now route titles through the helper before they reach the JSONL or the UI. Tail-read extractor: `extractLastJsonStringFields(text, primaryKey, otherKeys, lineContains)` reads multiple fields from the same matching line in a single pass. Two separate tail scans could return a mismatched pair (primary from a newer record, secondary from an older one with only the primary set); the new helper guarantees the pair is atomic. Validates a proper closing quote on the primary value so a crash-truncated trailing record can't win the latest-match race. `readLastJsonStringFieldsSync` is its file-reading wrapper — same tail-window fast path and full-file fallback as the single-field version, plus a `MAX_FULL_SCAN_BYTES = 64MB` cap so a corrupt multi-GB session file can't freeze the picker. Session reads now open with `O_NOFOLLOW` (falls back to plain RDONLY on Windows where the constant isn't exposed) — defense in depth against a symlink planted in `~/.qwen/projects/<proj>/chats/`. Character handling: `flattenToTail` on the LLM prompt drops a dangling low surrogate after `slice(-1000)` — otherwise a CJK supplementary char or emoji cut mid-pair produces invalid UTF-16 that some providers 400. `sanitizeTitle` applies the same surrogate scrub after max-length trim, and strips paired CJK brackets (`「」 『』 【】 〈〉 《》`) as whole units so a `【Draft】 Fix login` doesn't leave a dangling `】` after leading-char strip. `lineContains` in the title reader is tightened from the loose substring `'custom_title'` to `'"subtype":"custom_title"'` so user text containing the literal `custom_title` can't shadow a real record. Tests: 46 new unit tests across - `sessionTitle.test.ts` (22): success/all-failure-reasons, tool-call filter, tail-slice, surrogate scrub, ANSI/OSC-8 strip, CJK brackets. - `chatRecordingService.autoTitle.test.ts` (15): trigger/skip matrix, in-flight guard, abort propagation on finalize, manual/auto/legacy resume symmetry, cross-process race, env opt-out, retry-after- transient. - `sessionStorageUtils.test.ts` (13): single-pass extractor, straddle boundary, truncated trailing record, lineContains, multi-field atom. - `renameCommand.test.ts` (8): `--auto` success, all reasons, sentinel, unknown-flag hint, positional rejection, manual/SessionService fallbacks. * docs(session): design doc for auto session titles Matches the session-recap design doc shape (Overview / Triggers / Architecture / Prompt Design / History Filtering / Persistence / Concurrency / Configuration / Observability / Out of Scope) and adds a Security Hardening section unique to the title path — titles render directly in the picker and persist in user-readable JSONL, so LLM-returned control sequences are an attack surface the recap path doesn't have. Captures decisions a code-only reader has to reverse-engineer: - Why `maxAttempts: 1` (best-effort cosmetic metadata; no retry loop). - Why `autoTitleAttempts` cap is 3 (first turn can be pure tool-call). - Why the auto trigger does NOT fall back to the main model but session-recap does (auto-title fires on every turn; silently charging main-model tokens is a bill surprise). - Why `titleSource: undefined` stays unwritten on legacy records (no rewrite risks silently reclassifying user intent). - Why the cross-process re-read sits between the LLM await and the append (manual wins at both in-process and on-disk layers). - Why `finalize()`'s abort tolerates a controller swap (in-flight identity check). - Why JSON-schema function calling instead of tag extraction (avoid reasoning preamble bleed; cross-provider reliability). Placed at docs/design/session-title/ alongside session-recap, compact-mode, fork-subagent, and other per-feature design docs. No sidebar index update required — the design folder is unindexed. * test(rename): pin model choice in bare /rename kebab path Addresses reviewer feedback: the bare `/rename` model selection (`config.getFastModel() ?? config.getModel()`) had no test pinning it either way. Previous tests mocked `getHistory: []`, which exits the function before the model is ever chosen, so a silent regression to either direction (always-main or always-fast) would pass CI. Two explicit cases now: - fastModel set → `generateContent` called with `model: 'qwen-turbo'`. - fastModel unset → `generateContent` called with `model: 'main-model'`. The tests intentionally mock a non-empty history so the kebab path reaches the generateContent call site instead of bailing on empty input.
* fix(i18n): sync mismatched keys between en.js and zh.js (QwenLM#3503) Add 4 keys missing from en.js that are actively used in source code, add 5 missing Chinese translations to zh.js, integrate check-i18n into CI to prevent future drift, and skip JSON file write in CI to avoid dirtying the working tree. --- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…M#3509) * fix(cli): remove residual blank lines after MCP init completes (QwenLM#3095) ConfigInitDisplay rendered <Box marginTop={1}> plus a content line, so the live area grew by 2 rows during startup. When initialization finished and the component unmounted, Ink shrank the live area but the rows it had already committed to the terminal scrollback cannot be reclaimed, leaving a visible gap above the input. Move the MCP init status into the Footer's left-bottom status slot (always mounted, fixed height) so the live area height stays constant across the init → ready transition. The status participates in the existing priority chain: ctrlC / ctrlD / escape / vim / shell / autoAccept / configInit / hint. * fix(cli): suppress MCP init message when custom status line is active Audit follow-up. Previously the configInit branch preceded the suppressHint branch in the footer's left-bottom priority chain. With a custom status line configured, <Text>{null}</Text> collapses to zero rows in Ink, so the footer's bottom row went from 1 row during init to 0 rows after — a 1-row height oscillation that reintroduces the same scrollback-residue symptom the original fix eliminated in the default case. Swap the order so suppressHint short-circuits to null first: the init message now shares the hint's suppression rule, keeping the footer's height constant in every configuration. Also: - Gate the hook's return on isConfigInitialized directly instead of letting the effect clear state, avoiding a one-frame flash where the stale "Initializing..." message leaks through on the first render after init completes. - Cover the new behavior with three Footer tests, including a regression test for the custom-status-line case. * fix(cli): show MCP init progress even under a custom status line Reverting a UX trade-off introduced in the previous commit. That change suppressed the init message whenever a custom status line was active, arguing that <Text>{null}</Text> collapses to zero rows in Ink and any non-zero init row would re-create a one-row shrink on completion. Zero shrink was the wrong goal. Hiding init progress from users who have configured a status line is a real usability loss — the status line does not surface MCP connection state, so those users now see no feedback during startup. A one-time, one-line shrink on init completion is a far smaller regression than the original two-row scrollback residue this PR was created to fix, and strictly better than the silent alternative. Keep the init message in the left-bottom slot and let it sit above suppressHint in the priority chain. Update the regression test so that it pins the new behavior (init is visible with or without a status line) and prevents the suppression from being reintroduced. * fix(cli): keep MCP init progress visible in screen-reader mode Footer is gated behind !isScreenReaderEnabled, so moving the init message inside Footer silenced it for screen-reader users. Render the same message as a plain Text node in Composer when the screen reader is active — screen-reader users don't suffer from the live-area residual row issue that motivated the original move, so an independent node is safe for them. * refactor(cli): drop duplicated screen-reader init path and show progress under YOLO - ScreenReaderAppLayout already mounts <Footer /> directly, so the separate <Text> branch in Composer was producing a duplicated 'Connecting to MCP servers...' line in screen-reader mode. Remove it. - Move configInitMessage ahead of AutoAcceptIndicator in the footer's priority chain so users launched with YOLO / auto-accept-edits still see the ~1s startup progress; the approval-mode indicator takes over as soon as init finishes. - Add unit tests for useConfigInitMessage covering the idle, progress, reset, and unsubscribe paths.
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
…ased approach (QwenLM#3502) * feat(web-search): add GLM (ZhipuAI) web search provider - Add GlmProvider class implementing BaseWebSearchProvider using the ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search) - Support multiple search engines: search_std, search_pro, search_pro_sogou, search_pro_quark - Support optional config: maxResults, searchIntent, searchRecencyFilter, contentSize, searchDomainFilter - Truncate query to 70 characters per API limit - Register 'glm' in the provider discriminated union (types.ts) and createProvider() switch (index.ts) - Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class - Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts - Forward GLM_API_KEY in sandbox environment - Update provider priority list: Tavily > Google > GLM > DashScope - Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts - Update docs/developers/tools/web-search.md with GLM configuration, env vars, CLI args, pricing, and corrected DashScope billing info - Fix stale OAuth/free-tier references in web-search.md Closes QwenLM#3496 * docs(web-search): fix DashScope note and add GLM server-side limitations * fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency - DashScopeProvider.isAvailable() now checks config.apiKey instead of authType - Remove OAuth credential file reading and resource_url requirement - Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search - Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag - Forward DASHSCOPE_API_KEY into sandbox environment - Update integration test to detect DASHSCOPE_API_KEY - Update docs to reflect new API key based configuration * feat(web-search): remove built-in web search tool The web_search tool and all related provider implementations are removed. Web search functionality will be provided via MCP integrations instead, which is the direction the broader agent ecosystem is moving. Removed: - packages/core/src/tools/web-search/ (entire directory) - packages/cli/src/config/webSearch.ts - integration-tests/cli/web_search.test.ts - ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED - webSearch config in ConfigParams, Config class, settingsSchema - CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id, --glm-api-key, --dashscope-api-key, --web-search-default - Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys - web_search from rule-parser, permission-manager, speculation gate, microcompact tool set, and builtin-agents tool list * fix: remove websearch reference * docs: remove websearch tool * docs: add break change guide * fix review
) Selecting an older entry from input history via the arrow keys and pressing Enter now moves that entry to the most recent position, so the next Up press surfaces it first. Previously two bugs combined to keep stale copies in place: the history-navigation index was not reset on submit, and deduplication only collapsed consecutive repeats, leaving non-consecutive duplicates intact.
…3525) (QwenLM#3550) * refactor(core): make OpenAI converter stateless to prevent shared-state races Follow-up to QwenLM#3525. QwenLM#3516 showed that OpenAIContentConverter's long-lived per-pipeline state raced between concurrent streams; QwenLM#3525 scoped the streaming tool-call parser, this removes the remaining shared state. - OpenAIContentConverter is now a module of stand-alone functions; the exported symbol is a namespace object preserved for call-site compatibility. - New RequestContext (in types.ts, alongside PipelineConfig and ErrorHandler) carries model, modalities, startTime, and an optional per-stream toolCallParser. The pipeline builds one per request and threads it through every conversion call. - errorHandler drops duration/isStreaming; duration is recomputed from startTime at error time and troubleshooting text is uniform. - convertOpenAIChunkToGemini now throws if toolCallParser is missing so future misuse surfaces loudly instead of silently constructing a one-shot parser per chunk. * test(core): align timeout expectations
…'error' event (QwenLM#3481) * fix: strengthen error handling in launchBrowser to prevent unhandled events * fix: strengthen error handling with ChildProcess type and debugLogger * fix: use type-only import for ChildProcess
In ACP mode, the Mcp server list sent by the IDE client can include
SSE (type: "sse") and HTTP (type: "http") transports, but the previous
implementation only handled stdio servers via toStdioServer(). Non-stdio
servers were silently skipped (continue), so any SSE/HTTP-configured
MCP server would never be registered.
Changes:
- Add toSseServer() helper: detects type=="sse" servers and maps them
to MCPServerConfig(url=..., headers=...)
- Add toHttpServer() helper: detects type=="http" servers and maps them
to MCPServerConfig(httpUrl=..., headers=...)
- Refactor newSessionConfig() loop to handle all three transport types
- Declare mcpCapabilities: { sse: true, http: true } in agentCapabilities
so IDE clients know this agent supports these transports without needing
a transparent proxy
- Export the three helper functions for unit testing
Tests:
- Unit tests for toStdioServer / toSseServer / toHttpServer helpers
(type discrimination, mutual exclusion)
- Integration-style tests for QwenAgent.initialize() mcpCapabilities
- Integration-style tests for newSession() with SSE/HTTP MCP servers,
verifying MCPServerConfig is constructed with the correct arguments
(url vs httpUrl, headers passthrough, empty-headers → undefined)
Fixes QwenLM#3472
…#3463) * fix(cli): run ACP Agent tool calls concurrently (QwenLM#2516) When the model returns multiple Agent tool calls in a single turn, the ACP Session previously executed them sequentially in a plain for-loop, multiplying latency by the number of sub-agents spawned. Mirror the partition logic in coreToolScheduler.partitionToolCalls: consecutive Agent calls form a parallel batch (safe because sub-agents have no shared mutable state); any other tool forms its own sequential batch so the model's implicit ordering is preserved. Response-part ordering still matches the original functionCalls order. Add a focused test that uses controllable deferred executes to prove both Agent calls start before either resolves, and that the fed-back functionResponse ordering is stable regardless of resolution order. * Address PR QwenLM#3463 review: bound concurrency + robust test timing Two issues raised by the /review bot: 1. The raw Promise.all fan-out bypassed the bounded-concurrency guard that coreToolScheduler applies via QWEN_CODE_MAX_TOOL_CONCURRENCY. Replaced with an inline runBounded helper that mirrors core's runConcurrently (Promise.race on a bounded executing set, default cap 10), keeping in-order result collection. 2. The concurrency test used a 10-iteration microtask yield loop before asserting both execute() spies had been invoked. That's fragile — runTool's pre-execute path (build → getDefaultPermission → evaluatePermissionRules → permission branch → PreToolUseHook) has more await boundaries than 10 ticks guarantees, and the CI run reported call-a still at 0 invocations at the assertion point. Reworked the test to wait on an explicit `called` deferred that resolves *inside* the execute() mock body. Under sequential behaviour only one `called` would ever fire → `Promise.all([called-a, called-b])` deadlocks → vitest's per-test timeout surfaces the regression. Under the fix both fire before either result resolves. * fix(acp): degrade gracefully when AgentTool invocation has no eventEmitter The concurrency test for QwenLM#2516 timed out on CI with "Test timed out in 5000ms" after the `await Promise.all([called-a, called-b])` rewrite in the previous review-fix commit. The 5000ms wait was the symptom; the root cause is that neither `execute()` was ever being called. runTool's AgentTool branch was guarded with `'eventEmitter' in invocation`, which is a *key-presence* check. The test mock provides `{ eventEmitter: undefined, ... }` — the key exists (value undefined), the branch is entered, and `SubAgentTracker.setup` immediately throws inside `eventEmitter.on(...)`. The try/catch in runTool swallows the throw and returns an error response, so `invocation.execute()` never runs, `called[id].resolve()` never fires, and the test deadlocks. The earlier review commit (4519c5f) interpreted the CI symptom as "10 microtask yields aren't enough" and rewrote the assertion around a deferred `Promise.all`. But the old test's `toHaveBeenCalledTimes(1)` failure with 0 invocations was already the same bug — execute was never called. The new formulation just converted the visible failure from an assertion mismatch into a timeout. Switch the guard to a truthy check against `invocation.eventEmitter`. Semantics for real AgentTool are unchanged — `agent.ts:392` declares `readonly eventEmitter: AgentEventEmitter = new AgentEventEmitter()`, so production always enters the branch. The only new behavior is that incomplete invocations (or test mocks) skip SubAgentTracker setup cleanly instead of crashing. `subAgentCleanupFunctions` stays `[]`, so the cleanup forEach at the success/error paths is a no-op.
…d 9;5u output (QwenLM#3544) * fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output When a Kitty-capable terminal (iTerm2, Kitty, WezTerm) is used, the CLI enables the Kitty keyboard protocol at startup via ESC[>1u. On exit, the protocol must be disabled with ESC[<u to restore the terminal's default key encoding. Failing to do so leaves the terminal in Kitty mode: any subsequent Ctrl+C press is encoded as ESC[99;5u, and since the shell does not understand this sequence, it echoes the trailing '9;5u' as garbled text. Root cause: kittyProtocolDetector registered cleanup handlers for 'exit' and 'SIGTERM', but omitted SIGINT. A process terminated via SIGINT (e.g. kill -INT <pid>, a parent process sending SIGINT, or certain process managers) would exit without disabling the protocol. Fix: 1. Add process.on('SIGINT', disableProtocol) alongside the existing 'exit' and 'SIGTERM' handlers in kittyProtocolDetector.ts. 2. Export a new disableKittyProtocol() function for explicit call sites. 3. Call disableKittyProtocol() in the registerCleanup callback in gemini.tsx before instance.unmount(), so the disable sequence is written while stdout is fully operational regardless of exit path. Fixes QwenLM#3528 * fix(test): add disableKittyProtocol to kittyProtocolDetector mock
# Conflicts: # packages/core/src/services/chatRecordingService.ts
This was referenced Apr 28, 2026
This was referenced Apr 28, 2026
5 tasks
Owner
Author
Conflict Group 1This PR shares modified functions with 31 other PR(s): #1, #10, #100, #106, #107, #109, #112, #113, #114, #117, #14, #17, #18, #2, #21, #22, #26, #31, #36, #42, #46, #52, #6, #61, #7, #71, #75, #86, #90, #94, #96. These PRs should be reviewed as a batch — merging one may affect the others.
Posted by codegraph-ai conflict detection. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
When developers use AI agents to write code, there is no way to distinguish AI-generated changes from human-authored ones in git history. This creates challenges for:
This PR adds automatic, per-file AI contribution tracking that attaches structured metadata to commits without polluting commit messages.
How it works
Example output
{ "version": 1, "generator": "Qwen-Coder", "files": { "src/services/commitAttribution.ts": { "aiCharsAdded": 3200, "aiCharsRemoved": 0, "aiCreated": true, "aiContributionPercent": 100 }, "src/tools/shell.ts": { "aiCharsAdded": 1500, "aiCharsRemoved": 0, "aiCreated": false, "aiContributionPercent": 100 } }, "summary": { "totalAiCharsAdded": 4700, "totalAiCharsRemoved": 0, "totalFilesTouched": 2, "overallAiPercent": 100 } }Design decisions
Edge case behavior
Changed files
New files
Modified files
Test plan
Automated
commitAttribution.test.ts: 27 tests (character contribution, snapshot/restore, baselines, generated-file detection, surface, prompt counters)attributionTrailer.test.ts: 7 tests (shell-safe note command, size guard, formatting)tools/edit.test.ts/write-file.test.ts/shell.test.ts: 136 tests (integration with recordEdit and attachCommitAttribution)core/client.test.ts: 69 tests incl. 3 asserting attribution snapshot fires on UserQuery + ToolResult and skips RetryEnd-to-end (scratch repo + real CLI + real LLM)
aiChars/aiPercent: 100/surface: clihumanChars > 0, percent reflects real ratiopackage-lock.json) excluded fromfiles, appears inexcludedGenerateddiff-tree --rootand records correctlygeneral.gitCoAuthor: false→ commit succeeds with no git notes writtengh pr create --body "..."→ body auto-appended with🤖 Generated with Qwen Code--resume→ commit in turn 2 → notes contain the turn-1 edits (requires the ToolResult-turn snapshot fix in this PR)clearAttributions(false)on failed commit preservesgetPromptsSinceLastCommit()counter; next successful commit resets it