




























@@ -343,10 +343,14 @@ type DrainableTui = {
343343};
344344};
345345346+const TUI_SHUTDOWN_DRAIN_MAX_MS = 500;
347+const TUI_SHUTDOWN_DRAIN_IDLE_MS = 100;
348+const TUI_SHUTDOWN_HARD_EXIT_MS = 2000;
349+346350export async function drainAndStopTuiSafely(tui: DrainableTui): Promise<void> {
347351if (typeof tui.terminal?.drainInput === "function") {
348352try {
349-await tui.terminal.drainInput();
353+await tui.terminal.drainInput(TUI_SHUTDOWN_DRAIN_MAX_MS, TUI_SHUTDOWN_DRAIN_IDLE_MS);
350354} catch {
351355// Best-effort only. A failed drain should not skip terminal shutdown.
352356}
@@ -355,6 +359,7 @@ export async function drainAndStopTuiSafely(tui: DrainableTui): Promise<void> {
355359}
356360357361type CtrlCAction = "clear" | "warn" | "exit";
362+type TuiCtrlCAction = CtrlCAction | "force-exit";
358363359364export function resolveCtrlCAction(params: {
360365hasInput: boolean;
@@ -381,6 +386,23 @@ export function resolveCtrlCAction(params: {
381386};
382387}
383388389+export function resolveTuiCtrlCAction(params: {
390+hasInput: boolean;
391+now: number;
392+lastCtrlCAt: number;
393+exitRequested?: boolean;
394+wasDisconnected?: boolean;
395+exitWindowMs?: number;
396+}): { action: TuiCtrlCAction; nextLastCtrlCAt: number } {
397+if (params.exitRequested === true) {
398+return { action: "force-exit", nextLastCtrlCAt: params.lastCtrlCAt };
399+}
400+if (params.wasDisconnected === true) {
401+return { action: "exit", nextLastCtrlCAt: params.lastCtrlCAt };
402+}
403+return resolveCtrlCAction(params);
404+}
405+384406export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
385407const isLocalMode = opts.local === true || opts.backend !== undefined;
386408const config = opts.config ?? getRuntimeConfig();
@@ -1086,15 +1108,26 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
10861108});
1087110910881110const deferredFinish = createDeferredTuiFinish();
1111+const forceExit = () => {
1112+try {
1113+process.stderr.write("openclaw tui forcing exit\n");
1114+} catch {
1115+// Best effort only; force exit must not depend on stderr.
1116+}
1117+process.exit(130);
1118+};
10891119const requestExit = (result?: Partial<TuiResult>) => {
10901120if (exitRequested) {
1121+forceExit();
10911122return;
10921123}
10931124exitRequested = true;
10941125exitResult = {
10951126exitReason: result?.exitReason ?? "exit",
10961127 ...(result?.crestodianMessage ? { crestodianMessage: result.crestodianMessage } : {}),
10971128};
1129+const hardExitTimer = setTimeout(forceExit, TUI_SHUTDOWN_HARD_EXIT_MS);
1130+hardExitTimer.unref?.();
10981131client.stop();
10991132void drainAndStopTuiSafely(tui)
11001133.catch((err) => {
@@ -1107,6 +1140,7 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
11071140}
11081141})
11091142.finally(() => {
1143+clearTimeout(hardExitTimer);
11101144deferredFinish.requestFinish();
11111145});
11121146};
@@ -1169,11 +1203,17 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
11691203};
11701204const handleCtrlC = () => {
11711205const now = Date.now();
1172-const decision = resolveCtrlCAction({
1206+const decision = resolveTuiCtrlCAction({
11731207hasInput: editor.getText().trim().length > 0,
11741208 now,
11751209 lastCtrlCAt,
1210+ exitRequested,
1211+ wasDisconnected,
11761212});
1213+if (decision.action === "force-exit") {
1214+forceExit();
1215+return;
1216+}
11771217lastCtrlCAt = decision.nextLastCtrlCAt;
11781218if (decision.action === "clear") {
11791219editor.setText("");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。