[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
297
components/AIChatSidePanelHelpers.ts
Normal file
297
components/AIChatSidePanelHelpers.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
resolveAgentModelSelection,
|
||||
type AgentModelPreset,
|
||||
type ExternalAgentConfig,
|
||||
} from '../infrastructure/ai/types';
|
||||
import { getExternalAgentSdkBackend } from '../infrastructure/ai/managedAgents';
|
||||
import { canonicalizeEffortEncodedModelId } from '../infrastructure/ai/composerPicker';
|
||||
|
||||
export { canonicalizeEffortEncodedModelId };
|
||||
|
||||
export type SdkRuntimeModelCatalog = {
|
||||
currentModelId: string | null;
|
||||
models: AgentModelPreset[];
|
||||
};
|
||||
|
||||
export type SdkRuntimeModelCacheEntry = SdkRuntimeModelCatalog & {
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type SdkRuntimeModelCacheOptions = {
|
||||
ttlMs?: number;
|
||||
maxEntries?: number;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
type SdkRuntimeModelRefreshOptions = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
const SDK_RUNTIME_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const SDK_RUNTIME_MODEL_CACHE_MAX_ENTRIES = 64;
|
||||
// Keep in sync with main-process SDK_MODEL_CACHE_ENV_KEYS: profile-affecting
|
||||
// env must bust the renderer cache so we re-query after OpenCode config switches.
|
||||
const MODEL_CACHE_ENV_HINTS = [
|
||||
'HOME',
|
||||
'USERPROFILE',
|
||||
'XDG_CONFIG_HOME',
|
||||
'OPENCODE_BIN',
|
||||
'OPENCODE_CONFIG',
|
||||
'OPENCODE_CONFIG_DIR',
|
||||
'OPENCODE_CONFIG_CONTENT',
|
||||
'CLAUDE_CODE_EXECUTABLE',
|
||||
'CODEBUDDY_CODE_PATH',
|
||||
'CURSOR_API_KEY',
|
||||
'NETCATTY_CURSOR_AUTH_MODE',
|
||||
'NETCATTY_CURSOR_CLI_BIN',
|
||||
] as const;
|
||||
|
||||
function cloneCatalog(catalog: SdkRuntimeModelCatalog): SdkRuntimeModelCatalog {
|
||||
return {
|
||||
currentModelId: catalog.currentModelId ?? null,
|
||||
models: [...catalog.models],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSdkRuntimeModelCatalog(catalog: SdkRuntimeModelCatalog): SdkRuntimeModelCatalog {
|
||||
return {
|
||||
currentModelId: catalog.currentModelId ?? null,
|
||||
models: Array.isArray(catalog.models)
|
||||
? catalog.models.filter((model): model is AgentModelPreset => Boolean(model?.id))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Cursor auth-mode env for list-models IPC.
|
||||
* Mirrors run-turn `buildAgentEnvWithStoredApiKey` (without decrypting API keys):
|
||||
* persisted `agent.env` strips NETCATTY_CURSOR_* via sanitization, so list-models
|
||||
* must re-inject from `cursorAuthMode` / `command` or main defaults to api-key.
|
||||
*/
|
||||
export function buildCursorListModelsAgentEnv(agent: {
|
||||
env?: Record<string, string>;
|
||||
cursorAuthMode?: 'cli-login' | 'api-key';
|
||||
command?: string;
|
||||
}): Record<string, string> | undefined {
|
||||
const env = { ...(agent.env ?? {}) };
|
||||
const authMode = agent.cursorAuthMode === 'cli-login' ? 'cli-login' : 'api-key';
|
||||
env.NETCATTY_CURSOR_AUTH_MODE = authMode;
|
||||
const cliBin = String(agent.command || '').trim();
|
||||
if (authMode === 'cli-login' && cliBin && cliBin !== 'cursor') {
|
||||
env.NETCATTY_CURSOR_CLI_BIN = cliBin;
|
||||
}
|
||||
return Object.keys(env).length > 0 ? env : undefined;
|
||||
}
|
||||
|
||||
export function buildSdkRuntimeModelCacheKey(agent: {
|
||||
id: string;
|
||||
command?: string;
|
||||
sdkBackend?: string;
|
||||
acpCommand?: string;
|
||||
env?: Record<string, string>;
|
||||
codexRuntime?: 'sdk' | 'app-server';
|
||||
grokRuntime?: 'acp' | 'streaming-json';
|
||||
cursorAuthMode?: 'cli-login' | 'api-key';
|
||||
}): string {
|
||||
const sdkBackend = agent.sdkBackend || agent.acpCommand || '';
|
||||
const envHints = MODEL_CACHE_ENV_HINTS.map((key) => `${key}=${agent.env?.[key] ?? ''}`);
|
||||
// cursorAuthMode is the source of truth when NETCATTY_CURSOR_AUTH_MODE was
|
||||
// stripped from persisted env; include it so toggling auth mode busts cache.
|
||||
const cursorAuth = sdkBackend === 'cursor'
|
||||
? (agent.cursorAuthMode === 'cli-login' ? 'cli-login' : 'api-key')
|
||||
: '';
|
||||
const grokRuntime = sdkBackend === 'grok'
|
||||
? (agent.grokRuntime === 'streaming-json' ? 'streaming-json' : 'acp')
|
||||
: '';
|
||||
return [agent.id, sdkBackend, agent.command ?? '', agent.codexRuntime ?? 'sdk', grokRuntime, cursorAuth, ...envHints].join('\u0000');
|
||||
}
|
||||
|
||||
export function createSdkRuntimeModelCache(options: SdkRuntimeModelCacheOptions = {}) {
|
||||
const ttlMs = options.ttlMs ?? SDK_RUNTIME_MODEL_CACHE_TTL_MS;
|
||||
const maxEntries = Math.max(1, options.maxEntries ?? SDK_RUNTIME_MODEL_CACHE_MAX_ENTRIES);
|
||||
const now = options.now ?? (() => Date.now());
|
||||
const entries = new Map<string, SdkRuntimeModelCacheEntry>();
|
||||
const inFlight = new Map<string, Promise<SdkRuntimeModelCatalog>>();
|
||||
|
||||
const pruneEntries = () => {
|
||||
const currentTime = now();
|
||||
for (const [key, entry] of entries) {
|
||||
if (currentTime - entry.updatedAt >= ttlMs) {
|
||||
entries.delete(key);
|
||||
}
|
||||
}
|
||||
while (entries.size > maxEntries) {
|
||||
const oldestKey = entries.keys().next().value;
|
||||
if (oldestKey === undefined) break;
|
||||
entries.delete(oldestKey);
|
||||
}
|
||||
};
|
||||
|
||||
const touchEntry = (key: string, entry: SdkRuntimeModelCacheEntry) => {
|
||||
entries.delete(key);
|
||||
entries.set(key, entry);
|
||||
};
|
||||
|
||||
return {
|
||||
read(key: string): SdkRuntimeModelCacheEntry | null {
|
||||
pruneEntries();
|
||||
const entry = entries.get(key);
|
||||
if (entry) touchEntry(key, entry);
|
||||
return entry ? { ...cloneCatalog(entry), updatedAt: entry.updatedAt } : null;
|
||||
},
|
||||
size(): number {
|
||||
pruneEntries();
|
||||
return entries.size;
|
||||
},
|
||||
refresh(
|
||||
key: string,
|
||||
load: () => Promise<SdkRuntimeModelCatalog>,
|
||||
refreshOptions: SdkRuntimeModelRefreshOptions = {},
|
||||
): Promise<SdkRuntimeModelCatalog> {
|
||||
pruneEntries();
|
||||
const cached = entries.get(key);
|
||||
if (!refreshOptions.force && cached && now() - cached.updatedAt < ttlMs) {
|
||||
touchEntry(key, cached);
|
||||
return Promise.resolve(cloneCatalog(cached));
|
||||
}
|
||||
|
||||
const existing = inFlight.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
const promise = Promise.resolve(load())
|
||||
.then((catalog) => {
|
||||
const normalized = normalizeSdkRuntimeModelCatalog(catalog);
|
||||
if (normalized.models.length === 0 && !normalized.currentModelId) {
|
||||
return cached ? cloneCatalog(cached) : cloneCatalog(normalized);
|
||||
}
|
||||
touchEntry(key, { ...cloneCatalog(normalized), updatedAt: now() });
|
||||
pruneEntries();
|
||||
return cloneCatalog(normalized);
|
||||
})
|
||||
.finally(() => {
|
||||
if (inFlight.get(key) === promise) {
|
||||
inFlight.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
inFlight.set(key, promise);
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const sdkRuntimeModelCache = createSdkRuntimeModelCache();
|
||||
|
||||
export function mergeFallbackThinkingLevels(
|
||||
runtime: AgentModelPreset[],
|
||||
fallbacks: AgentModelPreset[],
|
||||
): AgentModelPreset[] {
|
||||
if (runtime.length === 0 || fallbacks.length === 0) return runtime;
|
||||
const byId = new Map(fallbacks.map((preset) => [preset.id, preset]));
|
||||
let changed = false;
|
||||
const next = runtime.map((preset) => {
|
||||
if (preset.thinkingLevels?.length) return preset;
|
||||
const fallback = byId.get(preset.id);
|
||||
if (!fallback?.thinkingLevels?.length) return preset;
|
||||
changed = true;
|
||||
return {
|
||||
...preset,
|
||||
thinkingLevels: [...fallback.thinkingLevels],
|
||||
...(fallback.defaultThinkingLevel
|
||||
? { defaultThinkingLevel: fallback.defaultThinkingLevel }
|
||||
: {}),
|
||||
...(fallback.encodeDefaultThinking === false
|
||||
? { encodeDefaultThinking: false }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
return changed ? next : runtime;
|
||||
}
|
||||
|
||||
export function agentModelPresetsShallowEqual(
|
||||
left: AgentModelPreset[] | undefined,
|
||||
right: AgentModelPreset[] | undefined,
|
||||
): boolean {
|
||||
if (left === right) return true;
|
||||
if (!left || !right || left.length !== right.length) return false;
|
||||
return left.every((preset, index) => {
|
||||
const other = right[index];
|
||||
return (
|
||||
preset.id === other.id
|
||||
&& preset.name === other.name
|
||||
&& preset.defaultThinkingLevel === other.defaultThinkingLevel
|
||||
&& (preset.thinkingLevels ?? []).join('\0') === (other.thinkingLevels ?? []).join('\0')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function modelPresetMatchesId(preset: AgentModelPreset, modelId: string): boolean {
|
||||
const canonical = canonicalizeEffortEncodedModelId(modelId);
|
||||
if (preset.thinkingLevels?.length) {
|
||||
return preset.id === canonical
|
||||
|| preset.thinkingLevels.some((level) => `${preset.id}/${level}` === canonical);
|
||||
}
|
||||
return preset.id === canonical;
|
||||
}
|
||||
|
||||
export function modelPresetsContainId(presets: AgentModelPreset[], modelId: string): boolean {
|
||||
return presets.some((preset) => modelPresetMatchesId(preset, modelId));
|
||||
}
|
||||
|
||||
export function normalizeStoredAgentModelSelection(
|
||||
storedModelId: string | null | undefined,
|
||||
presets: AgentModelPreset[],
|
||||
): string | undefined {
|
||||
if (!storedModelId) return undefined;
|
||||
const canonical = canonicalizeEffortEncodedModelId(storedModelId);
|
||||
const preset = presets.find((candidate) => modelPresetMatchesId(candidate, canonical));
|
||||
if (!preset) return undefined;
|
||||
return canonical === preset.id
|
||||
? resolveAgentModelSelection(preset)
|
||||
: canonical;
|
||||
}
|
||||
|
||||
export function shouldLoadSdkRuntimeModels(agent?: ExternalAgentConfig): boolean {
|
||||
const sdkBackend = getExternalAgentSdkBackend(agent);
|
||||
return (sdkBackend === 'codex' && agent?.codexRuntime === 'app-server')
|
||||
|| sdkBackend === 'claude'
|
||||
|| sdkBackend === 'copilot'
|
||||
|| sdkBackend === 'cursor'
|
||||
|| sdkBackend === 'codebuddy'
|
||||
|| sdkBackend === 'opencode'
|
||||
|| sdkBackend === 'grok';
|
||||
}
|
||||
|
||||
export function shouldAdoptSdkCurrentModel(
|
||||
currentModelId: string | null | undefined,
|
||||
storedModelId: string | null | undefined,
|
||||
runtimePresets: AgentModelPreset[],
|
||||
): boolean {
|
||||
if (!currentModelId) return false;
|
||||
return !storedModelId
|
||||
|| runtimePresets.length === 0
|
||||
|| !modelPresetsContainId(runtimePresets, storedModelId);
|
||||
}
|
||||
|
||||
export function normalizeSdkRuntimeModelPresets(
|
||||
models: AgentModelPreset[],
|
||||
currentModelId: string | null | undefined,
|
||||
): AgentModelPreset[] {
|
||||
if (models.length > 0) return models;
|
||||
if (!currentModelId) return [];
|
||||
return [{ id: currentModelId, name: currentModelId }];
|
||||
}
|
||||
|
||||
export function shouldUseStoredAgentModel(
|
||||
storedModelId: string | null | undefined,
|
||||
presets: AgentModelPreset[],
|
||||
agent?: ExternalAgentConfig,
|
||||
): boolean {
|
||||
if (!storedModelId) return false;
|
||||
return modelPresetsContainId(presets, storedModelId)
|
||||
|| (presets.length === 0 && shouldLoadSdkRuntimeModels(agent));
|
||||
}
|
||||
|
||||
export function generateId(): string {
|
||||
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user