Merge branch 'improvements' of https://github.com/777genius/claude_agent_teams_ui into improvements
This commit is contained in:
commit
dbb418ad64
3 changed files with 131 additions and 20 deletions
|
|
@ -1,3 +1,2 @@
|
|||
ignoredBuiltDependencies:
|
||||
- electron
|
||||
- esbuild
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue