[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:
157
application/state/useToolAccessGuidanceState.ts
Normal file
157
application/state/useToolAccessGuidanceState.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { AIToolIntegrationMode } from '../../infrastructure/ai/types';
|
||||
import { STORAGE_KEY_AI_EXTERNAL_MCP_ENABLED } from '../../infrastructure/config/storageKeys';
|
||||
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
||||
import { AI_STATE_CHANGED_EVENT } from './aiStateEvents';
|
||||
import { readExternalMcpStoredEnabled } from './useExternalMcpToggleState';
|
||||
|
||||
/** Right after an enable toggle the runtime may still be starting; poll briefly. */
|
||||
const EXTERNAL_MCP_READY_RETRY_LIMIT = 3;
|
||||
const EXTERNAL_MCP_READY_RETRY_DELAY_MS = 1200;
|
||||
/**
|
||||
* After the fast retry budget is exhausted, keep polling at the shared runtime
|
||||
* status cadence while the persisted switch and the runtime disagree, so a
|
||||
* persistently enabled runtime that takes longer than ~3.6s to reach `running`
|
||||
* still converges the guidance without an explicit state event.
|
||||
*/
|
||||
const EXTERNAL_MCP_SLOW_POLL_DELAY_MS = 3000;
|
||||
|
||||
export type ToolAccessGuidanceState = {
|
||||
/** Path of the local Netcatty skill file (Skills + CLI mode). */
|
||||
skillPath: string | null;
|
||||
/** Human-readable CLI command prefix shown under the skill path. */
|
||||
commandPrefix: string;
|
||||
/** Launcher path of the External MCP host (null while not enabled/ready). */
|
||||
mcpLauncherPath: string | null;
|
||||
/** Discovery file path handed to the external MCP host. */
|
||||
mcpDiscoveryPath: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves Tool Access guidance data for the settings view.
|
||||
*
|
||||
* Owns the External MCP lifecycle synchronization (status fetch, persisted
|
||||
* enable-key subscriptions and start-up retry polling) so view components only
|
||||
* render the resolved guidance state. The status paths are only trusted while
|
||||
* External MCP is actually enabled and running: the host keeps reporting
|
||||
* launcher and discovery paths as soon as the switch is on (even while the
|
||||
* runtime is still starting or after an idle shutdown).
|
||||
*/
|
||||
export function useToolAccessGuidanceState(mode: AIToolIntegrationMode): ToolAccessGuidanceState {
|
||||
const [skillPath, setSkillPath] = useState<string | null>(null);
|
||||
const [commandPrefix, setCommandPrefix] = useState('');
|
||||
const [mcpLauncherPath, setMcpLauncherPath] = useState<string | null>(null);
|
||||
const [mcpDiscoveryPath, setMcpDiscoveryPath] = useState<string | null>(null);
|
||||
|
||||
const refreshMcpStatus = useCallback(async (): Promise<string | null | undefined> => {
|
||||
const status = await netcattyBridge.get()?.externalMcpGetStatus?.();
|
||||
if (!status?.ok) return undefined;
|
||||
// Do not trust `enabled` alone: the main process flips `enabled` before
|
||||
// host credentials are resolved and the discovery file is written, and it
|
||||
// keeps reporting the configured discovery/launcher paths while starting.
|
||||
// Only treat the runtime as guidance-ready once it is actually running,
|
||||
// otherwise the prompt could reference a discovery file that does not
|
||||
// exist yet (and a launcher that exits because of it).
|
||||
const ready = Boolean(status.enabled) && status.state === 'running';
|
||||
const launcherPath = ready ? status.launcherPath ?? null : null;
|
||||
const discoveryPath = ready ? status.discoveryPath ?? null : null;
|
||||
setMcpLauncherPath(launcherPath);
|
||||
setMcpDiscoveryPath(discoveryPath);
|
||||
return launcherPath && discoveryPath ? launcherPath : null;
|
||||
}, []);
|
||||
|
||||
// Initial one-shot fetch for the skills mode.
|
||||
useEffect(() => {
|
||||
if (mode !== 'skills') return;
|
||||
let cancelled = false;
|
||||
void netcattyBridge
|
||||
.get()
|
||||
?.aiSkillsCliGetInvocation?.()
|
||||
.then((result) => {
|
||||
if (cancelled || !result?.ok) return;
|
||||
setSkillPath(result.skillPath ?? null);
|
||||
setCommandPrefix(result.commandPrefix || '');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mode]);
|
||||
|
||||
// Keep MCP guidance in sync with the shared persisted enable switch and
|
||||
// retry while the runtime finishes starting (fast retries, then a slower
|
||||
// fallback poll until switch and runtime agree). The fetch also runs on
|
||||
// mount through the retrying `refetch()` path: when the settings window
|
||||
// mounts while a persistently enabled runtime is still being restored, the
|
||||
// startup reconciliation emits no enable-key event, so without an initial
|
||||
// retrying fetch the fallback prompt would stay stale after the runtime
|
||||
// reaches `running`.
|
||||
useEffect(() => {
|
||||
if (mode === 'skills') return;
|
||||
let cancelled = false;
|
||||
let retryTimer: number | undefined;
|
||||
let retries = 0;
|
||||
|
||||
const refetch = async () => {
|
||||
if (cancelled) return;
|
||||
const storedEnabled = readExternalMcpStoredEnabled();
|
||||
const readyLauncherPath = await refreshMcpStatus().catch(() => undefined);
|
||||
if (cancelled) return;
|
||||
// The persisted switch is off but the status check failed: drop any
|
||||
// cached launcher/discovery paths. The stored credentials were revoked
|
||||
// by the disable, and falling through to the comparison below would see
|
||||
// "off" vs "not ready" as synchronized and keep a copyable prompt that
|
||||
// references the revoked credentials.
|
||||
if (!storedEnabled && readyLauncherPath === undefined) {
|
||||
setMcpLauncherPath(null);
|
||||
setMcpDiscoveryPath(null);
|
||||
}
|
||||
// Retry while the fetched runtime status disagrees with the persisted
|
||||
// switch: after an enable the runtime may still be starting, and the
|
||||
// disable event is emitted before the lifecycle IPC settles, so a stale
|
||||
// "enabled" status can survive the immediate refetch.
|
||||
const statusReady = readyLauncherPath != null;
|
||||
if (storedEnabled === statusReady) return;
|
||||
if (retries < EXTERNAL_MCP_READY_RETRY_LIMIT) {
|
||||
retries += 1;
|
||||
retryTimer = window.setTimeout(() => {
|
||||
void refetch();
|
||||
}, EXTERNAL_MCP_READY_RETRY_DELAY_MS);
|
||||
return;
|
||||
}
|
||||
// Startup can take longer than the fast retry budget (a persistent
|
||||
// runtime restoring while the settings view mounts): the startup
|
||||
// reconciliation emits no state event, so keep a slower fallback poll
|
||||
// running until switch and runtime agree again.
|
||||
retryTimer = window.setTimeout(() => {
|
||||
void refetch();
|
||||
}, EXTERNAL_MCP_SLOW_POLL_DELAY_MS);
|
||||
};
|
||||
|
||||
const refetchOnChange = () => {
|
||||
retries = 0;
|
||||
if (retryTimer) window.clearTimeout(retryTimer);
|
||||
void refetch();
|
||||
};
|
||||
const handleAIStateChanged = (event: Event) => {
|
||||
const key = (event as CustomEvent<{ key?: string }>).detail?.key;
|
||||
if (key !== STORAGE_KEY_AI_EXTERNAL_MCP_ENABLED) return;
|
||||
refetchOnChange();
|
||||
};
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key !== STORAGE_KEY_AI_EXTERNAL_MCP_ENABLED) return;
|
||||
refetchOnChange();
|
||||
};
|
||||
|
||||
window.addEventListener(AI_STATE_CHANGED_EVENT, handleAIStateChanged);
|
||||
window.addEventListener('storage', handleStorage);
|
||||
refetchOnChange();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryTimer) window.clearTimeout(retryTimer);
|
||||
window.removeEventListener(AI_STATE_CHANGED_EVENT, handleAIStateChanged);
|
||||
window.removeEventListener('storage', handleStorage);
|
||||
};
|
||||
}, [mode, refreshMcpStatus]);
|
||||
|
||||
return { skillPath, commandPrefix, mcpLauncherPath, mcpDiscoveryPath };
|
||||
}
|
||||
Reference in New Issue
Block a user