Merge branch 'improvements' of https://github.com/777genius/claude_agent_teams_ui into improvements

This commit is contained in:
iliya 2026-02-23 17:29:47 +02:00
commit dbb418ad64
3 changed files with 131 additions and 20 deletions

View file

@ -1,3 +1,2 @@
ignoredBuiltDependencies:
- electron
- esbuild

View file

@ -3,6 +3,15 @@ import * as os from 'os';
import * as path from 'path';
async function isExecutable(filePath: string): Promise<boolean> {
if (process.platform === 'win32') {
try {
const stat = await fs.promises.stat(filePath);
return stat.isFile();
} catch {
return false;
}
}
try {
await fs.promises.access(filePath, fs.constants.X_OK);
return true;
@ -11,6 +20,46 @@ async function isExecutable(filePath: string): Promise<boolean> {
}
}
function stripSurroundingQuotes(value: string): string {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function getWindowsExecutableExtensions(): string[] {
const raw = process.env.PATHEXT;
if (!raw) {
return ['.exe', '.cmd', '.bat', '.com'];
}
const exts = raw
.split(';')
.map((ext) => ext.trim())
.filter((ext) => ext.length > 0)
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
.map((ext) => ext.toLowerCase());
return Array.from(new Set(exts));
}
function expandWindowsBinaryNames(binaryName: string): string[] {
const trimmed = binaryName.trim();
if (!trimmed) {
return [];
}
const ext = path.extname(trimmed);
if (ext) {
return [trimmed];
}
const exts = getWindowsExecutableExtensions();
const withExt = exts.map((e) => `${trimmed}${e}`);
return [...withExt, trimmed];
}
async function collectNvmCandidates(): Promise<string[]> {
const nvmNodeRoot = path.join(os.homedir(), '.nvm', 'versions', 'node');
let versions: string[];
@ -33,16 +82,53 @@ async function resolveFromPathEnv(binaryName: string): Promise<string | null> {
}
const pathParts = rawPath.split(path.delimiter);
const binaryNames =
process.platform === 'win32' ? expandWindowsBinaryNames(binaryName) : [binaryName];
for (const part of pathParts) {
if (!part) {
continue;
}
const candidate = path.join(part, binaryName);
const cleanedPart = stripSurroundingQuotes(part);
if (!cleanedPart) {
continue;
}
for (const name of binaryNames) {
const candidate = path.join(cleanedPart, name);
if (await isExecutable(candidate)) {
return candidate;
}
}
}
return null;
}
async function resolveFromExplicitPath(inputPath: string): Promise<string | null> {
const trimmed = inputPath.trim();
if (!trimmed) {
return null;
}
if (await isExecutable(trimmed)) {
return trimmed;
}
if (process.platform !== 'win32') {
return null;
}
if (path.extname(trimmed)) {
return null;
}
for (const ext of getWindowsExecutableExtensions()) {
const candidate = `${trimmed}${ext}`;
if (await isExecutable(candidate)) {
return candidate;
}
}
return null;
}
@ -52,22 +138,45 @@ export class ClaudeBinaryResolver {
static async resolve(): Promise<string | null> {
if (cachedPath !== undefined) return cachedPath;
const platformBinaryName = process.platform === 'win32' ? 'claude.cmd' : 'claude';
const fromPath = await resolveFromPathEnv(platformBinaryName);
const overrideRaw = process.env.CLAUDE_CLI_PATH?.trim();
if (overrideRaw) {
const looksLikePath =
path.isAbsolute(overrideRaw) || overrideRaw.includes('\\') || overrideRaw.includes('/');
const resolvedOverride = looksLikePath
? await resolveFromExplicitPath(overrideRaw)
: await resolveFromPathEnv(overrideRaw);
if (resolvedOverride) {
cachedPath = resolvedOverride;
return cachedPath;
}
}
const baseBinaryName = 'claude';
const fromPath = await resolveFromPathEnv(baseBinaryName);
if (fromPath) {
cachedPath = fromPath;
return cachedPath;
}
const candidates: string[] = [
path.join(os.homedir(), '.npm-global', 'bin', platformBinaryName),
path.join(os.homedir(), '.npm', 'bin', platformBinaryName),
const platformBinaryNames =
process.platform === 'win32' ? expandWindowsBinaryNames(baseBinaryName) : [baseBinaryName];
const candidateDirs: string[] = [
path.join(os.homedir(), '.npm-global', 'bin'),
path.join(os.homedir(), '.npm', 'bin'),
process.platform === 'win32'
? path.join(process.env.APPDATA ?? '', 'npm', 'claude.cmd')
: '/usr/local/bin/claude',
process.platform === 'win32' ? '' : '/opt/homebrew/bin/claude',
? process.env.APPDATA
? path.join(process.env.APPDATA, 'npm')
: ''
: '/usr/local/bin',
process.platform === 'win32' ? '' : '/opt/homebrew/bin',
].filter((candidate) => candidate.length > 0);
const candidates = candidateDirs.flatMap((dir) =>
platformBinaryNames.map((name) => path.join(dir, name))
);
const nvmCandidates = process.platform === 'win32' ? [] : await collectNvmCandidates();
for (const candidate of [...candidates, ...nvmCandidates]) {
if (await isExecutable(candidate)) {

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import {
getReadSet as getReadSetStorage,
@ -9,8 +9,13 @@ export function useTeamMessagesRead(teamName: string): {
readSet: Set<string>;
markRead: (messageKey: string) => void;
} {
const [readSet, setReadSet] = useState<Set<string>>(() =>
teamName ? getReadSetStorage(teamName) : new Set()
const [version, setVersion] = useState(0);
const readSet = useMemo(
() => {
if (version < 0) return new Set<string>();
return teamName ? getReadSetStorage(teamName) : new Set<string>();
},
[teamName, version]
);
useEffect(() => {
@ -23,13 +28,11 @@ export function useTeamMessagesRead(teamName: string): {
const markRead = useCallback(
(messageKey: string) => {
if (!teamName) return;
setReadSet((prev) => {
if (prev.has(messageKey)) return prev;
const next = new Set(prev);
next.add(messageKey);
markReadStorage(teamName, messageKey, next);
return next;
});
const existing = getReadSetStorage(teamName);
if (existing.has(messageKey)) return;
existing.add(messageKey);
markReadStorage(teamName, messageKey, existing);
setVersion((v) => v + 1);
},
[teamName]
);