fix: restore interactive OpenAI REPL on React 18

This commit is contained in:
Kevin
2026-04-01 09:47:21 +08:00
parent e69cf0917e
commit 770e16dadb
7 changed files with 92 additions and 155 deletions

View File

@@ -1,8 +1,8 @@
import { c as _c } from "react-compiler-runtime"; import { c as _c } from "react-compiler-runtime";
import * as React from 'react'; import * as React from 'react';
import { use } from 'react';
import { Box } from '../ink.js'; import { Box } from '../ink.js';
import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js'; import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js';
import type { MemoryFileInfo } from '../utils/claudemd.js';
import { getMemoryFiles } from '../utils/claudemd.js'; import { getMemoryFiles } from '../utils/claudemd.js';
import { getGlobalConfig } from '../utils/config.js'; import { getGlobalConfig } from '../utils/config.js';
import { getActiveNotices, type StatusNoticeContext } from '../utils/statusNoticeDefinitions.js'; import { getActiveNotices, type StatusNoticeContext } from '../utils/statusNoticeDefinitions.js';
@@ -10,28 +10,53 @@ type Props = {
agentDefinitions?: AgentDefinitionsResult; agentDefinitions?: AgentDefinitionsResult;
}; };
let cachedMemoryFiles: MemoryFileInfo[] = [];
let memoryFilesPromise: Promise<void> | null = null;
async function loadMemoryFiles(): Promise<void> {
if (memoryFilesPromise) {
return memoryFilesPromise;
}
memoryFilesPromise = getMemoryFiles().then(files => {
cachedMemoryFiles = files;
}).finally(() => {
memoryFilesPromise = null;
});
return memoryFilesPromise;
}
/** /**
* StatusNotices contains the information displayed to users at startup. We have * StatusNotices contains the information displayed to users at startup. We have
* moved neutral or positive status to src/components/Status.tsx instead, which * moved neutral or positive status to src/components/Status.tsx instead, which
* users can access through /status. * users can access through /status.
*/ */
export function StatusNotices(t0) { export function StatusNotices(t0) {
const $ = _c(4); const $ = _c(8);
const { const {
agentDefinitions agentDefinitions
} = t0 === undefined ? {} : t0; } = t0 === undefined ? {} : t0;
const t1 = getGlobalConfig(); const [memoryFiles, setMemoryFiles] = React.useState(cachedMemoryFiles);
let t2; let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t2 = getMemoryFiles(); t1 = () => {
$[0] = t2; if (cachedMemoryFiles.length > 0) {
} else { setMemoryFiles(cachedMemoryFiles);
t2 = $[0]; return;
} }
void loadMemoryFiles().then(() => {
setMemoryFiles(cachedMemoryFiles);
});
};
$[0] = t1;
} else {
t1 = $[0];
}
React.useEffect(t1, [t1]);
const t2 = getGlobalConfig();
const context = { const context = {
config: t1, config: t2,
agentDefinitions, agentDefinitions,
memoryFiles: use(t2) memoryFiles
}; };
const activeNotices = getActiveNotices(context); const activeNotices = getActiveNotices(context);
if (activeNotices.length === 0) { if (activeNotices.length === 0) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -232,7 +232,7 @@ const reconciler = createReconciler<
unknown, unknown,
DOMElement, DOMElement,
HostContext, HostContext,
null, // UpdatePayload - not used in React 19 boolean,
NodeJS.Timeout, NodeJS.Timeout,
-1, -1,
null null
@@ -398,6 +398,14 @@ const reconciler = createReconciler<
): boolean { ): boolean {
return props['autoFocus'] === true return props['autoFocus'] === true
}, },
prepareUpdate(
_node: DOMElement,
_type: ElementNames,
oldProps: Props,
newProps: Props,
): boolean {
return oldProps !== newProps
},
commitMount(node: DOMElement): void { commitMount(node: DOMElement): void {
getFocusManager(node).handleAutoFocus(node) getFocusManager(node).handleAutoFocus(node)
}, },
@@ -422,9 +430,9 @@ const reconciler = createReconciler<
cleanupYogaNode(removeNode) cleanupYogaNode(removeNode)
getFocusManager(node).handleNodeRemoved(removeNode, node) getFocusManager(node).handleNodeRemoved(removeNode, node)
}, },
// React 19 commitUpdate receives old and new props directly instead of an updatePayload
commitUpdate( commitUpdate(
node: DOMElement, node: DOMElement,
_updatePayload: boolean,
_type: ElementNames, _type: ElementNames,
oldProps: Props, oldProps: Props,
newProps: Props, newProps: Props,

View File

@@ -596,7 +596,6 @@ export function REPL({
sshSession, sshSession,
thinkingConfig thinkingConfig
}: Props): React.ReactNode { }: Props): React.ReactNode {
logForDebugging('[REPL:render] function entry');
const isRemoteSession = !!remoteSessionConfig; const isRemoteSession = !!remoteSessionConfig;
// Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+ // Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
@@ -608,12 +607,6 @@ export function REPL({
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false; useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
// Log REPL mount/unmount lifecycle
useEffect(() => {
logForDebugging(`[REPL:mount] REPL mounted, disabled=${disabled}`);
return () => logForDebugging(`[REPL:unmount] REPL unmounting`);
}, [disabled]);
// Agent definition is state so /resume can update it mid-session // Agent definition is state so /resume can update it mid-session
const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition); const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition);
const toolPermissionContext = useAppState(s => s.toolPermissionContext); const toolPermissionContext = useAppState(s => s.toolPermissionContext);