Files
NetMesh/domain/codingCliTitleParse.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

121 lines
4.8 KiB
TypeScript

import type { CodingCliProviderId } from './codingCliProviders';
/** Braille dot-spinner frames used by Codex and several other agent TUIs. */
export const CODING_CLI_BRAILLE_SPINNER_FRAMES = [
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏',
] as const;
const BRAILLE_SPINNER_RE = /^[\s]+/u;
const ACTION_REQUIRED_PREFIX_RE = /^\[\s*[!.]\s*\]\s*(?:Action Required\s*)?/iu;
const LEADING_SEPARATOR_RE = /^[\s·.]+/u;
const CLAUDE_MARKERS = ['claude code', 'claude', 'anthropic'] as const;
const CODEX_STATUS_WORDS = ['working', 'thinking', 'ready', 'waiting'] as const;
const BUSY_STATUS_WORDS = ['working', 'thinking', 'running', 'compacting', 'generating'] as const;
const WAITING_STATUS_WORDS = ['waiting', 'permission', 'approval', 'confirm', 'input required'] as const;
export type CodingCliActivityPhase = 'idle' | 'busy' | 'waiting';
export function normalizeCodingCliTitle(title: string): string {
let normalized = title.trim().replace(BRAILLE_SPINNER_RE, '').trim();
normalized = normalized.replace(ACTION_REQUIRED_PREFIX_RE, '').trim();
normalized = normalized.replace(LEADING_SEPARATOR_RE, '').trim();
return normalized;
}
export function normalizeCodingCliDynamicTitleForStorage(title: string): string {
let normalized = title.trim().replace(BRAILLE_SPINNER_RE, '').trim();
normalized = normalized.replace(LEADING_SEPARATOR_RE, '').trim();
return normalized;
}
export function titleHasBrailleSpinner(title: string): boolean {
return CODING_CLI_BRAILLE_SPINNER_FRAMES.some((frame) => title.includes(frame));
}
export function titleIncludesPhrase(title: string, phrase: string): boolean {
const normalized = title.toLowerCase();
const needle = phrase.toLowerCase().trim();
if (!needle) return false;
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?:^|[^a-z0-9])${escaped}(?:[^a-z0-9]|$)`, 'i').test(normalized);
}
export function inferCodingCliProviderFromTitleSignals(title: string): CodingCliProviderId | undefined {
const raw = title.trim();
if (!raw) return undefined;
if (titleIncludesPhrase(raw, 'claude code') || raw.includes('✳') || titleIncludesPhrase(raw, 'claude')) {
return 'claude';
}
if (titleIncludesPhrase(raw, 'opencode')) return 'opencode';
if (titleIncludesPhrase(raw, 'codex') || titleIncludesPhrase(raw, 'chatgpt')) return 'codex';
if (titleIncludesPhrase(raw, 'github copilot') || titleIncludesPhrase(raw, 'copilot')) return 'copilot';
if (titleIncludesPhrase(raw, 'codebuddy')) return 'codebuddy';
if (titleIncludesPhrase(raw, 'gemini')) return 'gemini';
if (titleIncludesPhrase(raw, 'moonshot') || titleIncludesPhrase(raw, 'kimi')) return 'kimi';
if (titleIncludesPhrase(raw, 'factory droid') || titleIncludesPhrase(raw, 'factory ai')) return 'droid';
if (titleIncludesPhrase(raw, 'droid')) return 'droid';
if (titleIncludesPhrase(raw, 'cursor agent') || titleIncludesPhrase(raw, 'cursor')) return 'cursor';
const stripped = normalizeCodingCliTitle(raw).toLowerCase();
if (
titleHasBrailleSpinner(raw)
&& CODEX_STATUS_WORDS.some((word) => titleIncludesPhrase(stripped, word))
&& !CLAUDE_MARKERS.some((marker) => titleIncludesPhrase(raw, marker))
) {
return 'codex';
}
return undefined;
}
export function resolveCodingCliActivityPhase(
title: string | undefined,
providerId?: CodingCliProviderId,
): CodingCliActivityPhase {
const raw = title?.trim();
if (!raw || !providerId) return 'idle';
const normalized = normalizeCodingCliTitle(raw).toLowerCase();
if (WAITING_STATUS_WORDS.some((word) => titleIncludesPhrase(normalized, word))) {
return 'waiting';
}
if (titleHasBrailleSpinner(raw) || raw.includes('✳')) {
return 'busy';
}
if (BUSY_STATUS_WORDS.some((word) => titleIncludesPhrase(normalized, word))) {
return 'busy';
}
if (providerId === 'codex' && CODEX_STATUS_WORDS.includes(normalized as typeof CODEX_STATUS_WORDS[number])) {
return normalized === 'ready' ? 'idle' : 'busy';
}
return 'idle';
}
const SHELL_TITLE_RE = /^(?:bash|zsh|fish|pwsh|powershell|sh|nu|xonsh|cmd)(?:\s|$|[(@])/i;
const SHELL_PATH_TITLE_RE = /^(?:(?:[^@\s:]+@)?[^:\s]+:)?(?:~(?:\/|$)|\/|[A-Za-z]:[\\/])/;
/** Whether a shell-reported title no longer reflects an active coding CLI session. */
export function shouldClearCodingCliProviderForTitle(
title: string,
providerId: CodingCliProviderId,
): boolean {
const trimmed = title.trim();
if (!trimmed) return true;
const inferredId = inferCodingCliProviderFromTitleSignals(trimmed);
if (inferredId === providerId) return false;
if (inferredId) return true;
if (SHELL_TITLE_RE.test(trimmed)) return true;
if (SHELL_PATH_TITLE_RE.test(trimmed)) return true;
// Ambiguous titles (e.g. Codex project names) may still be an active agent session.
return false;
}