refactoring: switched to gpt-5-nano for commit message generation

Remove commitMessageModel from persisted settings and defaults
Eliminate UI controls and state for commitMessageModel in GitSettings and config store
Update backend and persistence layers to stop reading or writing commitMessageModel
This commit is contained in:
Bohdan Triapitsyn
2026-01-19 03:42:34 +02:00
parent ce1622b1e2
commit 494da1315c
8 changed files with 129 additions and 173 deletions
+71 -24
View File
@@ -15,6 +15,44 @@ use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
fn extract_json_object(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
let mut start = match trimmed.find('{') {
Some(index) => index,
None => return None,
};
while start < trimmed.len() {
let mut end = match trimmed[start..].find('}') {
Some(index) => start + index,
None => break,
};
loop {
let candidate = &trimmed[start..=end];
if serde_json::from_str::<Value>(candidate).is_ok() {
return Some(candidate.to_string());
}
end = match trimmed[end + 1..].find('}') {
Some(index) => end + 1 + index,
None => break,
};
}
start = match trimmed[start + 1..].find('{') {
Some(index) => start + 1 + index,
None => break,
};
}
None
}
const GIT_IDENTITY_STORAGE_FILE: &str = "git-identities.json";
const GIT_FILE_DIFF_TIMEOUT_MS: u64 = 15_000;
const GIT_LS_REMOTE_TIMEOUT_MS: u64 = 5_000;
@@ -2225,32 +2263,17 @@ Diff summary:
{}"#,
diff_summaries
);
let settings = state.settings().load().await.unwrap_or(serde_json::Value::Null);
let raw_model = settings.get("commitMessageModel")
.and_then(|v| v.as_str())
.unwrap_or("");
let model_candidate = raw_model
.split('/')
.last()
.unwrap_or(raw_model)
.trim();
let model = if model_candidate.is_empty() {
"big-pickle"
} else {
model_candidate
};
let model = "gpt-5-nano";
// 3. Call API
let client = Client::new();
let res = client
.post("https://opencode.ai/zen/v1/chat/completions")
.post("https://opencode.ai/zen/v1/responses")
.json(&serde_json::json!({
"model": model,
"messages": [{ "role": "user", "content": prompt }],
"max_tokens": 3000,
"input": [{ "role": "user", "content": prompt }],
"max_output_tokens": 1000,
"stream": false,
"reasoning": {
"effort": "low"
@@ -2265,8 +2288,12 @@ Diff summary:
}
let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?;
let raw_content = body["choices"][0]["message"]["content"]
.as_str()
let raw_content = body["output"]
.as_array()
.and_then(|items| items.iter().find(|item| item["type"] == "message"))
.and_then(|item| item["content"].as_array())
.and_then(|content| content.iter().find(|entry| entry["type"] == "output_text"))
.and_then(|entry| entry["text"].as_str())
.unwrap_or("")
.trim();
@@ -2278,8 +2305,28 @@ Diff summary:
.trim_end_matches("```")
.trim();
let message: GeneratedCommitMessage =
serde_json::from_str(cleaned).map_err(|e| format!("Failed to parse AI response: {}", e))?;
let extracted = extract_json_object(cleaned);
Ok(CommitMessageResponse { message })
let mut last_error: Option<String> = None;
if let Some(candidate) = extracted.as_deref() {
if candidate.starts_with('{') || candidate.starts_with('[') {
match serde_json::from_str::<GeneratedCommitMessage>(candidate) {
Ok(message) => return Ok(CommitMessageResponse { message }),
Err(err) => last_error = Some(err.to_string()),
}
}
}
if cleaned.starts_with('{') || cleaned.starts_with('[') {
match serde_json::from_str::<GeneratedCommitMessage>(cleaned) {
Ok(message) => return Ok(CommitMessageResponse { message }),
Err(err) => last_error = Some(err.to_string()),
}
}
Err(format!(
"Failed to parse AI response: {}",
last_error.unwrap_or_else(|| "unknown error".to_string())
))
}
@@ -265,15 +265,6 @@ fn sanitize_settings_update(payload: &Value) -> Value {
result_obj.insert("defaultGitIdentityId".to_string(), json!(trimmed));
}
}
if let Some(Value::String(s)) = obj.get("commitMessageModel") {
let trimmed = s.trim();
if trimmed.is_empty() {
result_obj.insert("commitMessageModel".to_string(), Value::Null);
} else {
result_obj.insert("commitMessageModel".to_string(), json!(trimmed));
}
}
// Boolean fields
if let Some(Value::Bool(b)) = obj.get("gitmojiEnabled") {
result_obj.insert("gitmojiEnabled".to_string(), json!(b));
@@ -1,64 +1,23 @@
import React from 'react';
import { RiInformationLine } from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
import { updateDesktopSettings } from '@/lib/persistence';
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
import { useConfigStore } from '@/stores/useConfigStore';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
const FALLBACK_PROVIDER_ID = 'opencode';
const FALLBACK_MODEL_ID = 'big-pickle';
const getDisplayModel = (
storedModel: string | undefined,
providers: Array<{ id: string; models: Array<{ id: string }> }>
): { providerId: string; modelId: string } => {
if (storedModel) {
const parts = storedModel.split('/');
if (parts.length === 2 && parts[0] && parts[1]) {
return { providerId: parts[0], modelId: parts[1] };
}
}
const fallbackProvider = providers.find(p => p.id === FALLBACK_PROVIDER_ID);
if (fallbackProvider?.models.some(m => m.id === FALLBACK_MODEL_ID)) {
return { providerId: FALLBACK_PROVIDER_ID, modelId: FALLBACK_MODEL_ID };
}
const firstProvider = providers[0];
if (firstProvider?.models[0]) {
return { providerId: firstProvider.id, modelId: firstProvider.models[0].id };
}
return { providerId: '', modelId: '' };
};
export const GitSettings: React.FC = () => {
const settingsCommitMessageModel = useConfigStore((state) => state.settingsCommitMessageModel);
const setSettingsCommitMessageModel = useConfigStore((state) => state.setSettingsCommitMessageModel);
const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled);
const setSettingsGitmojiEnabled = useConfigStore((state) => state.setSettingsGitmojiEnabled);
const providers = useConfigStore((state) => state.providers);
const [isLoading, setIsLoading] = React.useState(true);
const opencodeProviders = React.useMemo(() => {
return providers.filter((provider) => provider.id === FALLBACK_PROVIDER_ID);
}, [providers]);
const parsedModel = React.useMemo(() => {
const effectiveStoredModel = settingsCommitMessageModel?.startsWith(`${FALLBACK_PROVIDER_ID}/`)
? settingsCommitMessageModel
: undefined;
return getDisplayModel(effectiveStoredModel, opencodeProviders);
}, [settingsCommitMessageModel, opencodeProviders]);
// Load current settings
React.useEffect(() => {
const loadSettings = async () => {
try {
let data: { commitMessageModel?: string; gitmojiEnabled?: boolean } | null = null;
let data: { gitmojiEnabled?: boolean } | null = null;
// 1. Desktop runtime (Tauri)
if (isDesktopRuntime()) {
@@ -72,7 +31,6 @@ export const GitSettings: React.FC = () => {
const settings = result?.settings;
if (settings) {
data = {
commitMessageModel: typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel : undefined,
gitmojiEnabled: typeof (settings as Record<string, unknown>).gitmojiEnabled === 'boolean'
? ((settings as Record<string, unknown>).gitmojiEnabled as boolean)
: undefined,
@@ -96,10 +54,6 @@ export const GitSettings: React.FC = () => {
}
if (data) {
const model = typeof data.commitMessageModel === 'string' && data.commitMessageModel.trim().length > 0
? data.commitMessageModel.trim()
: undefined;
setSettingsCommitMessageModel(model);
if (typeof data.gitmojiEnabled === 'boolean') {
setSettingsGitmojiEnabled(data.gitmojiEnabled);
}
@@ -112,20 +66,7 @@ export const GitSettings: React.FC = () => {
}
};
loadSettings();
}, [setSettingsCommitMessageModel, setSettingsGitmojiEnabled]);
const handleModelChange = React.useCallback(async (providerId: string, modelId: string) => {
const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined;
setSettingsCommitMessageModel(newValue);
try {
await updateDesktopSettings({
commitMessageModel: newValue ?? '',
});
} catch (error) {
console.warn('Failed to save commit message model:', error);
}
}, [setSettingsCommitMessageModel]);
}, [setSettingsGitmojiEnabled]);
const handleGitmojiChange = React.useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
const enabled = event.target.checked;
@@ -160,20 +101,6 @@ export const GitSettings: React.FC = () => {
</div>
<div className="space-y-3">
<fieldset className="flex flex-col gap-1.5">
<legend className="typography-ui-label text-muted-foreground">Model for generation</legend>
<ModelSelector
providerId={parsedModel.providerId}
modelId={parsedModel.modelId}
onChange={handleModelChange}
allowedProviderIds={[FALLBACK_PROVIDER_ID]}
/>
<p className="typography-meta text-muted-foreground mt-1">
This model will be used to analyze diffs and suggest commit messages.
{!settingsCommitMessageModel && <> Default: <span className="text-foreground">opencode/big-pickle</span></>}
</p>
</fieldset>
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
@@ -374,7 +374,7 @@ export const useChatScrollManager = ({
? (container.querySelector(`[data-message-id="${persistedAnchor.anchorId}"]`) as HTMLElement | null)
: null;
const messageHeight = anchorElement?.offsetHeight ?? 0;
const restoredSpacerHeight = Math.max(0, persistedAnchor.spacerHeight - messageHeight);
const restoredSpacerHeight = Math.max(0, persistedAnchor.spacerHeight - (messageHeight - 50));
flushSync(() => {
setAnchorId(persistedAnchor.anchorId);
-1
View File
@@ -59,7 +59,6 @@ export type DesktopSettings = {
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
autoCreateWorktree?: boolean;
queueModeEnabled?: boolean;
commitMessageModel?: string; // format: "provider/model"
gitmojiEnabled?: boolean;
// User-added skills catalogs (persisted to ~/.config/openchamber/settings.json)
-8
View File
@@ -47,11 +47,6 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
} else {
localStorage.removeItem('pinnedDirectories');
}
if (settings.commitMessageModel) {
localStorage.setItem('commitMessageModel', settings.commitMessageModel);
} else {
localStorage.removeItem('commitMessageModel');
}
if (typeof settings.gitmojiEnabled === 'boolean') {
localStorage.setItem('gitmojiEnabled', String(settings.gitmojiEnabled));
} else {
@@ -271,9 +266,6 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.autoCreateWorktree === 'boolean') {
result.autoCreateWorktree = candidate.autoCreateWorktree;
}
if (typeof candidate.commitMessageModel === 'string' && candidate.commitMessageModel.length > 0) {
result.commitMessageModel = candidate.commitMessageModel;
}
if (typeof candidate.gitmojiEnabled === 'boolean') {
result.gitmojiEnabled = candidate.gitmojiEnabled;
}
-15
View File
@@ -25,7 +25,6 @@ interface OpenChamberDefaults {
defaultVariant?: string;
defaultAgent?: string;
autoCreateWorktree?: boolean;
commitMessageModel?: string;
gitmojiEnabled?: boolean;
}
@@ -39,7 +38,6 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
defaultVariant: settings?.defaultVariant,
defaultAgent: settings?.defaultAgent,
autoCreateWorktree: settings?.autoCreateWorktree,
commitMessageModel: settings?.commitMessageModel,
gitmojiEnabled: settings?.gitmojiEnabled,
};
}
@@ -54,7 +52,6 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : '';
const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : '';
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : '';
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
return {
@@ -62,7 +59,6 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined,
gitmojiEnabled,
};
}
@@ -83,7 +79,6 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : '';
const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : '';
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : '';
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
return {
@@ -91,7 +86,6 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined,
gitmojiEnabled,
};
} catch {
@@ -384,7 +378,6 @@ interface ConfigStore {
settingsDefaultVariant: string | undefined;
settingsDefaultAgent: string | undefined;
settingsAutoCreateWorktree: boolean;
settingsCommitMessageModel: string | undefined; // format: "provider/model"
settingsGitmojiEnabled: boolean;
activateDirectory: (directory: string | null | undefined) => Promise<void>;
@@ -402,7 +395,6 @@ interface ConfigStore {
setSettingsDefaultVariant: (variant: string | undefined) => void;
setSettingsDefaultAgent: (agent: string | undefined) => void;
setSettingsAutoCreateWorktree: (enabled: boolean) => void;
setSettingsCommitMessageModel: (model: string | undefined) => void;
setSettingsGitmojiEnabled: (enabled: boolean) => void;
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
@@ -447,7 +439,6 @@ export const useConfigStore = create<ConfigStore>()(
settingsDefaultVariant: undefined,
settingsDefaultAgent: undefined,
settingsAutoCreateWorktree: false,
settingsCommitMessageModel: undefined,
settingsGitmojiEnabled: false,
activateDirectory: async (directory) => {
@@ -892,7 +883,6 @@ export const useConfigStore = create<ConfigStore>()(
settingsDefaultVariant: openChamberDefaults.defaultVariant,
settingsDefaultAgent: openChamberDefaults.defaultAgent,
settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false,
settingsCommitMessageModel: openChamberDefaults.commitMessageModel,
settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false,
directoryScoped: {
...state.directoryScoped,
@@ -1314,10 +1304,6 @@ export const useConfigStore = create<ConfigStore>()(
set({ settingsAutoCreateWorktree: enabled });
},
setSettingsCommitMessageModel: (model) => {
set({ settingsCommitMessageModel: model });
},
setSettingsGitmojiEnabled: (enabled: boolean) => {
set({ settingsGitmojiEnabled: enabled });
},
@@ -1427,7 +1413,6 @@ export const useConfigStore = create<ConfigStore>()(
settingsDefaultVariant: state.settingsDefaultVariant,
settingsDefaultAgent: state.settingsDefaultAgent,
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
settingsCommitMessageModel: state.settingsCommitMessageModel,
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
}),
},
+55 -40
View File
@@ -285,6 +285,31 @@ const stripJsonMarkdownWrapper = (value) => {
return trimmed;
};
const extractJsonObject = (value) => {
if (typeof value !== 'string') {
return null;
}
const source = value.trim();
if (!source) {
return null;
}
let start = source.indexOf('{');
while (start !== -1) {
let end = source.indexOf('}', start + 1);
while (end !== -1) {
const candidate = source.slice(start, end + 1);
try {
JSON.parse(candidate);
return candidate;
} catch {
end = source.indexOf('}', end + 1);
}
}
start = source.indexOf('{', start + 1);
}
return null;
};
const OPENCHAMBER_DATA_DIR = process.env.OPENCHAMBER_DATA_DIR
? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
: path.join(os.homedir(), '.config', 'openchamber');
@@ -613,10 +638,6 @@ const sanitizeSettingsUpdate = (payload) => {
if (typeof candidate.autoCreateWorktree === 'boolean') {
result.autoCreateWorktree = candidate.autoCreateWorktree;
}
if (typeof candidate.commitMessageModel === 'string') {
const trimmed = candidate.commitMessageModel.trim();
result.commitMessageModel = trimmed.length > 0 ? trimmed : undefined;
}
if (typeof candidate.gitmojiEnabled === 'boolean') {
result.gitmojiEnabled = candidate.gitmojiEnabled;
}
@@ -847,7 +868,7 @@ const persistSettings = async (changes) => {
console.log(`[persistSettings] Successfully saved ${next.projects?.length || 0} projects to disk`);
return formatSettingsResponse(next);
});
return persistSettingsLock;
};
@@ -1377,7 +1398,7 @@ async function restartOpenCode() {
}
killProcessOnPort(portToKill);
// Brief delay to allow port release
await new Promise((resolve) => setTimeout(resolve, 250));
@@ -1388,7 +1409,7 @@ async function restartOpenCode() {
openCodePort = null;
syncToHmrState();
}
openCodeApiPrefixDetected = true;
openCodeApiPrefix = '';
if (openCodeApiDetectionTimer) {
@@ -2523,7 +2544,7 @@ async function main(options = {}) {
});
// ============== SKILL ENDPOINTS ==============
const {
getSkillSources,
discoverSkills,
@@ -2545,7 +2566,7 @@ async function main(options = {}) {
return res.status(400).json({ error });
}
const skills = discoverSkills(directory);
// Enrich with full sources info
const enrichedSkills = skills.map(skill => {
const sources = getSkillSources(skill.name, directory);
@@ -2554,7 +2575,7 @@ async function main(options = {}) {
sources
};
});
res.json({ skills: enrichedSkills });
} catch (error) {
console.error('Failed to list skills:', error);
@@ -2860,17 +2881,17 @@ async function main(options = {}) {
if (!directory) {
return res.status(400).json({ error });
}
const sources = getSkillSources(skillName, directory);
if (!sources.md.exists || !sources.md.dir) {
return res.status(404).json({ error: 'Skill not found' });
}
const content = readSkillSupportingFile(sources.md.dir, filePath);
if (content === null) {
return res.status(404).json({ error: 'File not found' });
}
res.json({ path: filePath, content });
} catch (error) {
console.error('Failed to read skill file:', error);
@@ -2942,14 +2963,14 @@ async function main(options = {}) {
if (!directory) {
return res.status(400).json({ error });
}
const sources = getSkillSources(skillName, directory);
if (!sources.md.exists || !sources.md.dir) {
return res.status(404).json({ error: 'Skill not found' });
}
writeSkillSupportingFile(sources.md.dir, filePath, content || '');
res.json({
success: true,
message: `File ${filePath} saved successfully`,
@@ -2969,14 +2990,14 @@ async function main(options = {}) {
if (!directory) {
return res.status(400).json({ error });
}
const sources = getSkillSources(skillName, directory);
if (!sources.md.exists || !sources.md.dir) {
return res.status(404).json({ error: 'Skill not found' });
}
deleteSkillSupportingFile(sources.md.dir, filePath);
res.json({
success: true,
message: `File ${filePath} deleted successfully`,
@@ -3378,26 +3399,19 @@ async function main(options = {}) {
.join('\n\n');
const prompt = `You are drafting git commit notes for this codebase. Respond in JSON of the shape {"subject": string, "highlights": string[]} (ONLY the JSON in response, no markdown wrappers or anything except JSON) with these rules:\n- subject follows our convention: type[optional-scope]: summary (examples: "feat: add diff virtualization", "fix(chat): restore enter key handling")\n- allowed types: feat, fix, chore, style, refactor, perf, docs, test, build, ci (choose the best match or fallback to chore)\n- summary must be imperative, concise, <= 70 characters, no trailing punctuation\n- scope is optional; include only when obvious from filenames/folders; do not invent scopes\n- focus on the most impactful user-facing change; if multiple capabilities ship together, align the subject with the dominant theme and use highlights to cover the other major outcomes\n- highlights array should contain 2-3 plain sentences (<= 90 chars each) that describe distinct features or UI changes users will notice (e.g. "Add per-file revert action in Changes list"). Avoid subjective benefit statements, marketing tone, repeating the subject, or referencing helper function names. Highlight additions such as new controls/buttons, new actions (e.g. revert), or stored state changes explicitly. Skip highlights if fewer than two meaningful points exist.\n- text must be plain (no markdown bullets); each highlight should start with an uppercase verb\n\nDiff summary:\n${diffSummaries}`;
const settings = await readSettingsFromDiskMigrated();
const rawModel = typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel.trim() : '';
const model = (() => {
if (!rawModel) return 'big-pickle';
const parts = rawModel.split('/').filter(Boolean);
const candidate = parts.length > 1 ? parts[parts.length - 1] : parts[0];
return candidate || 'big-pickle';
})();
const model = 'gpt-5-nano';
const completionTimeout = createTimeoutSignal(LONG_REQUEST_TIMEOUT_MS);
let response;
try {
response = await fetch('https://opencode.ai/zen/v1/chat/completions', {
response = await fetch('https://opencode.ai/zen/v1/responses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
max_tokens: 3000,
input: [{ role: 'user', content: prompt }],
max_output_tokens: 1000,
stream: false,
reasoning: {
effort: 'low'
@@ -3416,14 +3430,15 @@ async function main(options = {}) {
}
const data = await response.json();
const raw = data?.choices?.[0]?.message?.content?.trim();
const raw = data?.output?.find((item) => item?.type === 'message')?.content?.find((item) => item?.type === 'output_text')?.text?.trim();
if (!raw) {
return res.status(502).json({ error: 'No commit message returned by generator' });
}
const cleanedJson = stripJsonMarkdownWrapper(raw);
const candidates = [cleanedJson, raw].filter((candidate, index, array) => {
const extractedJson = extractJsonObject(cleanedJson) || extractJsonObject(raw);
const candidates = [cleanedJson, extractedJson, raw].filter((candidate, index, array) => {
return candidate && array.indexOf(candidate) === index;
});
@@ -4210,14 +4225,14 @@ async function main(options = {}) {
}
const dirents = await fsPromises.readdir(resolvedPath, { withFileTypes: true });
// Get gitignored paths if requested
let ignoredPaths = new Set();
if (respectGitignore) {
try {
// Get all entry paths to check (relative to resolvedPath for git check-ignore)
const pathsToCheck = dirents.map((d) => d.name);
if (pathsToCheck.length > 0) {
try {
// Use git check-ignore with paths as arguments
@@ -4227,13 +4242,13 @@ async function main(options = {}) {
cwd: resolvedPath,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
child.stdout.on('data', (data) => { stdout += data.toString(); });
child.on('close', () => resolve(stdout));
child.on('error', () => resolve(''));
});
result.split('\n').filter(Boolean).forEach((name) => {
const fullPath = path.join(resolvedPath, name.trim());
ignoredPaths.add(fullPath);
@@ -4246,16 +4261,16 @@ async function main(options = {}) {
// If git is not available, continue without gitignore filtering
}
}
const entries = await Promise.all(
dirents.map(async (dirent) => {
const entryPath = path.join(resolvedPath, dirent.name);
// Skip gitignored entries
if (respectGitignore && ignoredPaths.has(entryPath)) {
return null;
}
let isDirectory = dirent.isDirectory();
const isSymbolicLink = dirent.isSymbolicLink();