[2/4] Add commit message prompt and generator service - #299
Conversation
📝 WalkthroughWalkthroughAdds Git-context types and collector, COMMIT_MESSAGE support prompt, CommitMessageGenerator service (AI prompt building, optional profile selection, output cleaning), tests (unit + integration), global settings for commit-message config, and a telemetry event for generated commit messages. ChangesCommit Message Generation Feature
Sequence Diagram(s)sequenceDiagram
participant Caller
participant CommitMessageGenerator
participant ProviderSettingsManager
participant GitContextCollector
participant AICompletion
participant TelemetryService
Caller->>CommitMessageGenerator: generateMessage(params)
CommitMessageGenerator->>GitContextCollector: collect(options, specificFiles)
GitContextCollector-->>CommitMessageGenerator: GitContextCollection (changes, context, warnings)
CommitMessageGenerator->>ProviderSettingsManager: getProfile(commitMessageApiConfigId)?
ProviderSettingsManager-->>CommitMessageGenerator: profile settings / error
CommitMessageGenerator->>AICompletion: completePrompt(finalPrompt, selectedSettings)
AICompletion-->>CommitMessageGenerator: raw model output
CommitMessageGenerator->>CommitMessageGenerator: extractCommitMessage(raw)
CommitMessageGenerator->>TelemetryService: captureEvent(COMMIT_MSG_GENERATED)
CommitMessageGenerator-->>Caller: cleaned commit message
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
packages/types/src/global-settings.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. packages/types/src/telemetry.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. src/services/commit-message/CommitMessageGenerator.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/services/git-context/__tests__/GitContextCollector.spec.ts (1)
78-90: ⚡ Quick winAdd a regression test for staged-only entries in unstaged mode.
Please add a case like porcelain
M src/file.tsand assertgatherChanges({ staged: false })excludes it, so unstaged context can’t regress into index-state parsing.As per coding guidelines,
Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/git-context/__tests__/GitContextCollector.spec.ts` around lines 78 - 90, Add a regression unit test in GitContextCollector.spec.ts that ensures gatherChanges({ staged: false }) ignores staged-only entries: call mockGitCommand with a porcelain entry like "M src/file.ts" (porcelain staged marker) plus a null separator, instantiate new GitContextCollector(workspaceRoot) and call collector.gatherChanges({ staged: false }), then assert mockSpawn was invoked with ["status","--porcelain=v1","-z","--untracked-files=all"] and that the returned changes do NOT include the staged file (i.e., expect changes toEqual [] or only include unstaged entries); use existing helpers mockGitCommand and mockSpawn and reference GitContextCollector.gatherChanges to locate where to add the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/services/commit-message/CommitMessageGenerator.ts`:
- Around line 193-197: The extractCommitMessage method currently only strips
code fences and surrounding quotes and can leave leading prose; update
CommitMessageGenerator.extractCommitMessage to split the cleaned text into
lines, locate the first line matching a conventional commit header (use a regex
like
/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s.+$/),
throw a clear error if none is found, and return the joined lines starting from
that header (trimmed) so any wrapper/lead-in text is dropped.
- Around line 47-57: In generateMessage (class CommitMessageGenerator) validate
the incoming gitContext from GenerateMessageParams for emptiness or "no-change"
(e.g., no staged/changed files, empty diff or summary) and fail fast by throwing
a clear Error (or returning a rejected Promise) before calling
callAIForCommitMessage or updating progress; include a descriptive message such
as "No changes to generate a commit message for" so callers get an immediate,
explicit failure instead of invoking the model.
In `@src/services/git-context/GitContextCollector.ts`:
- Around line 398-401: The code currently falls back to indexStatus when
computing statusCode, causing staged-only entries to be treated as unstaged;
update the logic in GitContextCollector where indexStatus and workingStatus are
read (the block that sets statusCode and calls getChangeStatusFromCode) so that
in unstaged-collection mode you only consider workingStatus (i.e., if
workingStatus is blank/space or "?" then skip this entry entirely instead of
using indexStatus), then call getChangeStatusFromCode with the
workingStatus-derived code; ensure the collector's unstaged-mode flag is checked
so staged-only entries are ignored.
---
Nitpick comments:
In `@src/services/git-context/__tests__/GitContextCollector.spec.ts`:
- Around line 78-90: Add a regression unit test in GitContextCollector.spec.ts
that ensures gatherChanges({ staged: false }) ignores staged-only entries: call
mockGitCommand with a porcelain entry like "M src/file.ts" (porcelain staged
marker) plus a null separator, instantiate new
GitContextCollector(workspaceRoot) and call collector.gatherChanges({ staged:
false }), then assert mockSpawn was invoked with
["status","--porcelain=v1","-z","--untracked-files=all"] and that the returned
changes do NOT include the staged file (i.e., expect changes toEqual [] or only
include unstaged entries); use existing helpers mockGitCommand and mockSpawn and
reference GitContextCollector.gatherChanges to locate where to add the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 66632ad6-85b7-4abe-918e-e408217f4772
📒 Files selected for processing (11)
packages/types/src/global-settings.tspackages/types/src/telemetry.tssrc/services/commit-message/CommitMessageGenerator.tssrc/services/commit-message/__tests__/CommitMessageGeneration.integration.spec.tssrc/services/commit-message/__tests__/CommitMessageGenerator.spec.tssrc/services/commit-message/types/core.tssrc/services/git-context/GitContextCollector.tssrc/services/git-context/__tests__/GitContextCollector.spec.tssrc/services/git-context/index.tssrc/services/git-context/types.tssrc/shared/support-prompt.ts
cb54fa6 to
48a5bad
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/services/commit-message/CommitMessageGenerator.ts (1)
234-239:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStrip wrapper prose before returning the commit message.
Line 235-239 removes only outer fences/quotes, so responses like “Here is your commit message:” can still leak into output. Extract from the first conventional-commit header (and fail fast if absent) to keep the generator contract strict.
Proposed fix
private extractCommitMessage(response: string): string { - const cleaned = response.trim() - const withoutCodeBlocks = cleaned.replace(/^```[a-zA-Z0-9_-]*\r?\n/, "").replace(/\r?\n```$/, "") - const withoutQuotes = withoutCodeBlocks.replace(/^["'`]|["'`]$/g, "") - return withoutQuotes.trim() + const cleaned = response.trim() + const withoutCodeFences = cleaned.replace(/```[a-zA-Z0-9_-]*\r?\n?/g, "").replace(/```/g, "") + const withoutQuotes = withoutCodeFences.replace(/^["'`]|["'`]$/g, "").trim() + const lines = withoutQuotes.split(/\r?\n/) + const conventionalHeader = + /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s.+$/ + const headerIndex = lines.findIndex((line) => conventionalHeader.test(line.trim())) + + if (headerIndex === -1) { + throw new Error("AI response did not contain a conventional commit header") + } + + return lines.slice(headerIndex).join("\n").trim() }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/commit-message/CommitMessageGenerator.ts` around lines 234 - 239, In extractCommitMessage, strip code fences and surrounding quotes, then split into lines and search for the first conventional commit header using a regex like /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s.+$/; if found, return the joined lines from that header onward trimmed, otherwise throw an Error("AI response did not contain a conventional commit header") to fail fast; update references in this function (variables like cleaned/withoutCodeFences/lines/conventionalHeader/headerIndex) accordingly so only the commit message from the first conventional header is returned.
🧹 Nitpick comments (2)
src/services/commit-message/__tests__/CommitMessageGenerator.spec.ts (2)
185-202: ⚡ Quick winAdd explicit surrounding-quote cleanup coverage.
This spec validates fence stripping, but not removal of surrounding quotes from model output. Add a case like
"fix(core): normalize output"and assert the returned message is unquoted.As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/commit-message/__tests__/CommitMessageGenerator.spec.ts` around lines 185 - 202, Add a unit test in CommitMessageGenerator.spec.ts that verifies surrounding quotes are stripped: mock completePrompt (used in the test that calls createGenerator() and generator.generateMessage) to return a quoted model output such as "\"fix(core): normalize output\"" (optionally combined with fences) and assert the returned message equals fix(core): normalize output (no surrounding quotes). This ensures the generateMessage cleanup logic removes surrounding quotes from model output in addition to stripping fences.
81-109: ⚡ Quick winMissing provider-failure unit test for
completePromptrejection path.Please add a test where
completePromptrejects and assertgenerateMessagepropagates/normalizes the error and does not callcaptureGenerated.As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."
Also applies to: 111-168
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/commit-message/__tests__/CommitMessageGenerator.spec.ts` around lines 81 - 109, Add a unit test to cover the provider-failure path by mocking completePrompt to reject and asserting generateMessage propagates/normalizes the error and does not call captureGenerated; specifically, in the CommitMessageGenerator.spec suite (use createGenerator to get the generator), set completePrompt.mockRejectedValueOnce(new Error("provider failure")) (or similar), call generator.generateMessage with the same args used in the existing test, expect the call to reject with the normalized error, assert captureGenerated was not called, and verify completePrompt was invoked once with defaultConfig and a prompt containing the same markers (e.g., "# Conventional Commit Message Generator" / "Follow repo commit rules.").
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/services/commit-message/CommitMessageGenerator.ts`:
- Around line 234-239: In extractCommitMessage, strip code fences and
surrounding quotes, then split into lines and search for the first conventional
commit header using a regex like
/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s.+$/;
if found, return the joined lines from that header onward trimmed, otherwise
throw an Error("AI response did not contain a conventional commit header") to
fail fast; update references in this function (variables like
cleaned/withoutCodeFences/lines/conventionalHeader/headerIndex) accordingly so
only the commit message from the first conventional header is returned.
---
Nitpick comments:
In `@src/services/commit-message/__tests__/CommitMessageGenerator.spec.ts`:
- Around line 185-202: Add a unit test in CommitMessageGenerator.spec.ts that
verifies surrounding quotes are stripped: mock completePrompt (used in the test
that calls createGenerator() and generator.generateMessage) to return a quoted
model output such as "\"fix(core): normalize output\"" (optionally combined with
fences) and assert the returned message equals fix(core): normalize output (no
surrounding quotes). This ensures the generateMessage cleanup logic removes
surrounding quotes from model output in addition to stripping fences.
- Around line 81-109: Add a unit test to cover the provider-failure path by
mocking completePrompt to reject and asserting generateMessage
propagates/normalizes the error and does not call captureGenerated;
specifically, in the CommitMessageGenerator.spec suite (use createGenerator to
get the generator), set completePrompt.mockRejectedValueOnce(new Error("provider
failure")) (or similar), call generator.generateMessage with the same args used
in the existing test, expect the call to reject with the normalized error,
assert captureGenerated was not called, and verify completePrompt was invoked
once with defaultConfig and a prompt containing the same markers (e.g., "#
Conventional Commit Message Generator" / "Follow repo commit rules.").
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 14270583-a7b4-466a-92ac-4e7e7c3b7019
📒 Files selected for processing (10)
packages/types/src/global-settings.tspackages/types/src/telemetry.tssrc/services/commit-message/CommitMessageGenerator.tssrc/services/commit-message/__tests__/CommitMessageGeneration.integration.spec.tssrc/services/commit-message/__tests__/CommitMessageGenerator.spec.tssrc/services/commit-message/types/core.tssrc/services/git-context/GitContextCollector.tssrc/services/git-context/__tests__/GitContextCollector.spec.tssrc/services/git-context/types.tssrc/shared/support-prompt.ts
|
Updated this PR with the review fixes and pushed the PR branch.
Stack handling:
Verification run:
Also tested first through commit-msg-gen-ext before copying the approved fix back to this PR branch. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
taltas
left a comment
There was a problem hiding this comment.
Some feedback on code styling and the prompt itself.
| let configToUse: ProviderSettings = apiConfiguration | ||
|
|
||
| if (commitMessageApiConfigId && listApiConfigMeta.find(({ id }) => id === commitMessageApiConfigId)) { | ||
| try { | ||
| await this.providerSettingsManager.initialize() | ||
| const { name: _, ...providerSettings } = await this.providerSettingsManager.getProfile({ | ||
| id: commitMessageApiConfigId, | ||
| }) | ||
|
|
||
| if (providerSettings.apiProvider) { | ||
| configToUse = providerSettings | ||
| } | ||
| } catch (error) { | ||
| this.dependencies.logger.warn( | ||
| `Failed to load commit message API profile ${commitMessageApiConfigId}; falling back to current API configuration`, | ||
| error, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Extract this out to it's own private function, there is too much going on here, it will make it harder to edit this in the future.
| let configToUse: ProviderSettings = apiConfiguration | |
| if (commitMessageApiConfigId && listApiConfigMeta.find(({ id }) => id === commitMessageApiConfigId)) { | |
| try { | |
| await this.providerSettingsManager.initialize() | |
| const { name: _, ...providerSettings } = await this.providerSettingsManager.getProfile({ | |
| id: commitMessageApiConfigId, | |
| }) | |
| if (providerSettings.apiProvider) { | |
| configToUse = providerSettings | |
| } | |
| } catch (error) { | |
| this.dependencies.logger.warn( | |
| `Failed to load commit message API profile ${commitMessageApiConfigId}; falling back to current API configuration`, | |
| error, | |
| ) | |
| } | |
| } | |
| const configToUse = await this.resolveCommitMessageApiConfiguration(contextProxy) |
...
private async resolveCommitMessageApiConfiguration(
contextProxy: CommitMessageContextProxy,
): Promise<ProviderSettings> {
const apiConfiguration = contextProxy.getProviderSettings()
const commitMessageApiConfigId = contextProxy.getValue("commitMessageApiConfigId") as string | undefined
const listApiConfigMeta = (contextProxy.getValue("listApiConfigMeta") || []) as Array<{ id: string }>
if (!commitMessageApiConfigId || !listApiConfigMeta.find(({ id }) => id === commitMessageApiConfigId)) {
return apiConfiguration
}
try {
await this.providerSettingsManager.initialize()
const { name: _, ...providerSettings } = await this.providerSettingsManager.getProfile({
id: commitMessageApiConfigId,
})
return providerSettings.apiProvider ? providerSettings : apiConfiguration
} catch (error) {
this.dependencies.logger.warn(
`Failed to load commit message API profile ${commitMessageApiConfigId}; falling back to current API configuration`,
error,
)
return apiConfiguration
}
}
| const targetPreviousMessage = previousMessage || this.previousCommitMessage | ||
|
|
||
| if (shouldGenerateDifferentMessage && targetPreviousMessage) { | ||
| const differentMessagePrefix = `# CRITICAL INSTRUCTION: GENERATE A COMPLETELY DIFFERENT COMMIT MESSAGE |
There was a problem hiding this comment.
Same with all this, we should extract the prompt into a different method.
i.e.
private buildRegenerationTemplate(
customSupportPrompts: Record<string, string>,
previousMessage: string,
): string {
const baseTemplate = supportPrompt.get(customSupportPrompts, "COMMIT_MESSAGE")
return `# CRITICAL INSTRUCTION: GENERATE A COMPLETELY DIFFERENT COMMIT MESSAGE
The user has requested a new commit message for the same changes.
The previous message was: "${previousMessage}"
YOU MUST create a message that is COMPLETELY DIFFERENT by:
- Using entirely different wording and phrasing
- Focusing on different aspects of the changes
- Using a different structure or format if appropriate
- Possibly using a different type or scope if justifiable
This is the MOST IMPORTANT requirement for this task.
${baseTemplate}
FINAL REMINDER: Your message MUST be COMPLETELY DIFFERENT from the previous message: "${previousMessage}". This is a critical requirement.`
}
| template: `# Conventional Commit Message Generator | ||
| ## System Instructions | ||
| You are an expert Git commit message generator that creates conventional commit messages based on provided Git changes. Analyze the provided git diff output and generate appropriate conventional commit messages following the specification. | ||
|
|
||
| \${customInstructions} | ||
|
|
||
| ## CRITICAL: Commit Message Output Rules | ||
| - DO NOT include any internal status indicators or bracketed metadata (e.g. "[Status: Active]", "[Context: Missing]") | ||
| - DO NOT include any task-specific formatting or artifacts from other rules | ||
| - ONLY Generate a clean conventional commit message as specified below | ||
|
|
||
| \${gitContext} | ||
|
|
||
| ## Conventional Commits Format | ||
| Generate commit messages following this exact structure: | ||
| \`\`\` | ||
| <type>[optional scope]: <description> | ||
| [optional body] | ||
| [optional footer(s)] | ||
| \`\`\` | ||
|
|
||
| ### Core Types (Required) | ||
| - **feat**: New feature or functionality (MINOR version bump) | ||
| - **fix**: Bug fix or error correction (PATCH version bump) | ||
|
|
||
| ### Additional Types (Extended) |
There was a problem hiding this comment.
This template should be in it's own file, it's way larger than the other prompts in this file.
| /** Returns the active provider settings used as the default generation profile. */ | ||
| getProviderSettings(): ProviderSettings | ||
| /** Reads a persisted extension setting by key. */ | ||
| getValue(key: any): unknown |
There was a problem hiding this comment.
We need better typing here
i.e.
getValue(key: "commitMessageApiConfigId"): string | undefined
getValue(key: "listApiConfigMeta"): Array<{ id: string; name?: string }> | undefined
getValue(key: "customSupportPrompts"): Record<string, string | undefined> | undefined
... etc
| this.validateGitContext(gitContext) | ||
|
|
||
| onProgress?.({ | ||
| message: "Generating commit message...", |
There was a problem hiding this comment.
This pattern is here several times. consider refactoring and naming the helpers appropriately based on the message.
| COMMIT_MESSAGE: { | ||
| template: `# Conventional Commit Message Generator | ||
| ## System Instructions | ||
| You are an expert Git commit message generator that creates conventional commit messages based on provided Git changes. Analyze the provided git diff output and generate appropriate conventional commit messages following the specification. |
There was a problem hiding this comment.
It's not just git diffs though, it's git context,
| You are an expert Git commit message generator that creates conventional commit messages based on provided Git changes. Analyze the provided git diff output and generate appropriate conventional commit messages following the specification. | |
| You are an expert Git commit message generator that creates conventional commit messages from the provided Git context. The context may include diffs, change summaries, diff stats, branch name, and recent commits. Use only that context as your source of truth. |
| ## CRITICAL: Commit Message Output Rules | ||
| - DO NOT include any internal status indicators or bracketed metadata (e.g. "[Status: Active]", "[Context: Missing]") | ||
| - DO NOT include any task-specific formatting or artifacts from other rules | ||
| - ONLY Generate a clean conventional commit message as specified below |
There was a problem hiding this comment.
More explicit hallucination rules
| - ONLY Generate a clean conventional commit message as specified below | |
| - Return ONLY a single clean conventional commit message | |
| - Do NOT wrap the answer in markdown, code fences, quotes, or commentary | |
| - If scope, body, or footers are uncertain, omit them instead of guessing | |
| - Do NOT invent breaking changes, issue references, scopes, or facts not supported by the Git context |
| - Start one blank line after description | ||
| - Explain the "what" and "why", not the "how" | ||
| - Wrap at 72 characters per line | ||
| - Use for complex changes requiring explanation |
There was a problem hiding this comment.
| - Use for complex changes requiring explanation | |
| - Use ONLY when the context clearly shows multiple meaningful changes or a non-obvious rationale |
See comment below about keeping it simple
| 5. For complex changes, include a detailed body explaining what and why | ||
| 6. Add appropriate footers for issue references or breaking changes |
There was a problem hiding this comment.
I prefer it go for single lines first, then attempt more detailed commits if necessary
| 5. For complex changes, include a detailed body explaining what and why | |
| 6. Add appropriate footers for issue references or breaking changes | |
| 5. Prefer a one-line commit message unless extra explanation is clearly needed | |
| 6. Add footers only when they are explicitly justified by the context |
Related GitHub Issue
Closes: #283, #284, #285
Part of: #145
Replaces part of: #146
Depends on: #298
Description
This is part 2 of the split commit-message generation stack. It adds the prompt and generation service that turns Git context from #298 into cleaned commit message text.
This PR keeps model invocation independent from VS Code SCM UI wiring and Settings UI. The goal is to make the prompt/generator behavior reviewable and testable as a service before the following PR connects it to the Source Control panel.
Key implementation details:
COMMIT_MESSAGEtoSupportPromptTypeandsupportPrompt.default.CommitMessageGenerator, which accepts Git context and calls Zoo's existing single-completion path.commitMessageApiConfigIdschema support for a dedicated commit-generation API profile.Reviewers should focus on the generator contract, provider profile fallback behavior, response cleanup, and whether the default prompt is appropriately constrained.
This aligns with Zoo Code's roadmap by improving developer workflow UX while reusing existing provider/prompt infrastructure rather than introducing a separate model path.
Test Procedure
Automated verification run before opening the split PR stack:
pnpm --dir src exec vitest run services/commit-message/__tests__/CommitMessageGenerator.spec.ts services/commit-message/__tests__/CommitMessageGeneration.integration.spec.tsReviewer verification:
src/shared/support-prompt.tsforCOMMIT_MESSAGEprompt behavior.src/services/commit-message/CommitMessageGenerator.tsfor provider profile fallback and response cleanup.Pre-Submission Checklist
Screenshots / Videos
Not applicable. This PR adds prompt and service-layer behavior only.
Documentation Updates
Additional Notes
Stack order:
This PR is opened against
mainbecause I cannot push to upstream stack branches from my fork. The meaningful incremental diff is after #298.Get in Touch
Discord: Mirrowel <@214161976534892545>
Summary by CodeRabbit
New Features
Tests