终止钮按 canStopTurn 显示:队列非空不再顶出终止按钮
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m24s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Frontend tests (pull_request) Successful in 1m57s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / Native shell tests (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m38s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m57s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m24s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Frontend tests (pull_request) Successful in 1m57s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / Native shell tests (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m38s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m57s
- `useDirectProjectTurnStatus` 新增派生判据 `canStopTurn`(`nativeRunning || commandInFlight`)与共享的 `canStopDirectProjectTurn`,写清它与 `displayBusy` 的分工:前者回答"终止这一脚踢给谁",后者回答"界面能不能声称空闲" - composer 的发送钮 / 终止钮按 `canStopTurn` 换;`busy` 继续管模型 / 附件 / 语音这些与当前这轮冲突的控件 - `cancelTurn` 的受理判据改用同一个函数:显示与受理同源,不再出现"按钮在、点了说没有回合" - `displayBusy` 保持三者并集不变(卡片仍要早于首个 token 显示"正在处理") - 测试:`directProjectTurnStatus` 补 `canStopTurn` 用例;appSurface 补"只有待发消息、没有回合在跑时不显示终止钮"(撤掉修复会红)
This commit is contained in:
@@ -272,6 +272,7 @@ export function DirectProjectChatView({
|
||||
pendingTurns={pendingTurns}
|
||||
composerNotice={composerNotice}
|
||||
busy={turnStatus.displayBusy}
|
||||
canStopTurn={turnStatus.canStopTurn}
|
||||
turnCancelling={turnCancelling}
|
||||
onCancelPendingTurn={(clientTurnId) =>
|
||||
void cancelPendingTurn(clientTurnId)
|
||||
|
||||
+12
-2
@@ -57,6 +57,7 @@ export function DirectProjectComposer({
|
||||
pendingTurns,
|
||||
composerNotice,
|
||||
busy,
|
||||
canStopTurn,
|
||||
turnCancelling,
|
||||
onCancelPendingTurn,
|
||||
onCancelTurn,
|
||||
@@ -72,7 +73,15 @@ export function DirectProjectComposer({
|
||||
attachmentsImporting: boolean;
|
||||
pendingTurns: readonly DirectPendingTurn[];
|
||||
composerNotice: string;
|
||||
/** 「界面忙不忙」:三者并集,用来锁住模型 / 附件 / 语音这些与当前这轮冲突的控件。 */
|
||||
busy: boolean;
|
||||
/**
|
||||
* 「现在点终止有没有作用对象」(见 `useDirectProjectTurnStatus` 的 `canStopTurn`)。
|
||||
*
|
||||
* 与 `busy` 分开:队列非空也算忙,但待发消息不属于「终止」——按 `busy` 显示终止钮会得到一个
|
||||
* 点了只会报「当前没有正在运行的回合」的按钮。发送钮 / 终止钮只按它换。
|
||||
*/
|
||||
canStopTurn: boolean;
|
||||
turnCancelling: boolean;
|
||||
onCancelPendingTurn: (clientTurnId: string) => void;
|
||||
onCancelTurn: () => void;
|
||||
@@ -159,7 +168,8 @@ export function DirectProjectComposer({
|
||||
},
|
||||
[onUploadFiles],
|
||||
);
|
||||
// 这个按钮只在非 busy 分支出现(busy 时由 ComposerStopButton 接管),所以不再带忙态逻辑。
|
||||
// 这个按钮只在"没有可终止对象"的分支出现(有的话由 ComposerStopButton 接管),
|
||||
// 所以不再带忙态逻辑:队列里有待发消息时它照样在,用户能继续排。
|
||||
const submitButton = (
|
||||
<button
|
||||
type="submit"
|
||||
@@ -246,7 +256,7 @@ export function DirectProjectComposer({
|
||||
onTranscript={(text) => composerRef.current?.insertText(text)}
|
||||
onNotice={setVoiceNotice}
|
||||
/>
|
||||
{busy ? (
|
||||
{canStopTurn ? (
|
||||
<ComposerStopButton
|
||||
cancelling={turnCancelling}
|
||||
onCancel={onCancelTurn}
|
||||
|
||||
+7
-1
@@ -44,6 +44,7 @@ import type { DirectThreadHistorySlice } from '../generated/DirectThreadHistoryS
|
||||
import type { DirectThreadItem } from '../generated/DirectThreadItem';
|
||||
import { directHistoryAnchorGateToWaitFor } from '../history/directHistoryAnchorGate';
|
||||
import { readDirectHistoryPages } from '../history/directHistoryPaging';
|
||||
import { canStopDirectProjectTurn } from './useDirectProjectTurnStatus';
|
||||
import { useDirectThreadChatSubscription } from './useDirectThreadChatSubscription';
|
||||
|
||||
export const MAX_CHAT_COMPOSER_ATTACHMENTS = 8;
|
||||
@@ -645,10 +646,15 @@ export function useDirectProjectChatController({
|
||||
async function cancelTurn() {
|
||||
if (turnCancelling) return;
|
||||
const invoke = resolveTauriInvoke();
|
||||
// 受理判据与终止钮的显示判据**同一个函数**(`useDirectProjectTurnStatus` 的 `canStopTurn`):
|
||||
// 两处各拼一遍布尔就会出现"按钮在、点了说没有回合"。
|
||||
if (
|
||||
!invoke ||
|
||||
!projectPath ||
|
||||
(!commandInFlightRef.current && !currentTurnRunning)
|
||||
!canStopDirectProjectTurn({
|
||||
turnRunning: currentTurnRunning,
|
||||
commandInFlight: commandInFlightRef.current,
|
||||
})
|
||||
) {
|
||||
onRuntimeError('当前没有正在运行的回合,无法终止。');
|
||||
return;
|
||||
|
||||
+25
@@ -24,6 +24,11 @@ import type {
|
||||
* 只要有一条成立就不能再接受新的发送。卡片读它而不是 `nativeRunning`:`turn.started`
|
||||
* 要等宿主应答返回才发出,只认原生真相会让模型首 token 之前那十来秒没有任何「正在处理」的
|
||||
* 交代(2026-09-24 口令)。
|
||||
* - `canStopTurn`:**「现在点终止有没有作用对象」的唯一判据**(`nativeRunning || commandInFlight`)。
|
||||
* 终止钮按它出现,命令受理时也按它把关:排队中还没放行的那几条不属于「终止」,`pendingCount`
|
||||
* 不该把终止钮顶出来(那是一个点了只会报「当前没有正在运行的回合」的按钮)。注意它与
|
||||
* `displayBusy` 是两个问题:`displayBusy` 回答「界面能不能声称空闲」,`canStopTurn` 回答
|
||||
* 「终止这一脚踢给谁」。
|
||||
* - `latestTurnState`:最新一轮在界面上的两态(投影结果);没有回合时为 null。
|
||||
*
|
||||
* 约定:新增"忙/在跑"类判据一律先落进这里,不要在组件里再拼布尔。
|
||||
@@ -36,9 +41,28 @@ export type DirectProjectTurnStatus = {
|
||||
commandInFlight: boolean;
|
||||
pendingCount: number;
|
||||
displayBusy: boolean;
|
||||
canStopTurn: boolean;
|
||||
latestTurnState: DirectChatTurnState | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 「现在点终止有没有作用对象」的唯一判据。
|
||||
*
|
||||
* 只有两种状态里有东西可终止:宿主已经放行(`nativeRunning`)、命令还在飞(`commandInFlight`,
|
||||
* 这一段结束前这一轮也可能已经成立)。待发消息属于「还没放行」,取消它走 chip 上的逐条入口,
|
||||
* 不属于「终止」。视图决定终止钮显不显示、控制器决定受理不受理,都读这一个函数——两处口径必须
|
||||
* 是同一份,否则就会出现「按钮在、点了说没有回合」。
|
||||
*/
|
||||
export function canStopDirectProjectTurn({
|
||||
turnRunning,
|
||||
commandInFlight,
|
||||
}: {
|
||||
turnRunning: boolean;
|
||||
commandInFlight: boolean;
|
||||
}): boolean {
|
||||
return Boolean(turnRunning) || Boolean(commandInFlight);
|
||||
}
|
||||
|
||||
export function deriveDirectProjectTurnStatus({
|
||||
turnRunning,
|
||||
commandInFlight,
|
||||
@@ -59,6 +83,7 @@ export function deriveDirectProjectTurnStatus({
|
||||
commandInFlight: inFlight,
|
||||
pendingCount,
|
||||
displayBusy: nativeRunning || inFlight || pendingCount > 0,
|
||||
canStopTurn: canStopDirectProjectTurn({ turnRunning, commandInFlight }),
|
||||
latestTurnState: latest ? latest.state : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -967,6 +967,34 @@ export function registerChatComposerControlTests() {
|
||||
});
|
||||
});
|
||||
|
||||
it('只有待发消息、没有回合在跑时不显示终止钮', async () => {
|
||||
// 队列非空但既没有在飞的命令、也没有在跑的回合(比如宿主还没认领队首):这时"终止"没有作用
|
||||
// 对象,界面不该给一个点了只会报「当前没有正在运行的回合」的按钮。
|
||||
const { surface, harness } = await openDirectCodexSurface({
|
||||
enqueue_direct_codex_turn: (args) => {
|
||||
harness.emitDirectThreadEvents({
|
||||
type: 'queue.enqueued',
|
||||
clientTurnId: String(args?.clientTurnId ?? ''),
|
||||
userItem: args?.userItem,
|
||||
at: 1,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
const composer = within(surface).getByLabelText('陶泥儿对话内容');
|
||||
await submitDirectTurn(surface, composer, '排着的那条');
|
||||
const queue = await within(surface).findByLabelText('待发送消息队列');
|
||||
await waitFor(() => {
|
||||
expect(within(queue).getByText('排着的那条')).not.toBeNull();
|
||||
});
|
||||
|
||||
// 队列非空仍然是"忙"(卡片照旧说正在处理),但发送入口是发送钮:用户可以继续排。
|
||||
expect(within(surface).queryByRole('button', { name: '终止' })).toBeNull();
|
||||
expect(
|
||||
await within(surface).findByRole('button', { name: '发送' }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('queues the next message on the host while a turn is already running', async () => {
|
||||
// 回合已经在跑(比如刚切回工作台):这时发送只做一件事——交给宿主入队,chip 由事件投影出来。
|
||||
let host: ReturnType<typeof hostQueue> | null = null;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { deriveDirectProjectTurnStatus } from '../src/view/project-development/chat/controller/useDirectProjectTurnStatus';
|
||||
import {
|
||||
canStopDirectProjectTurn,
|
||||
deriveDirectProjectTurnStatus,
|
||||
} from '../src/view/project-development/chat/controller/useDirectProjectTurnStatus';
|
||||
import type { DirectPendingTurn } from '../src/view/project-development/chat/conversation/directPendingTurns';
|
||||
import type { DirectChatTurn } from '../src/view/project-development/chat/conversation/directTurnPresentation';
|
||||
|
||||
@@ -75,6 +78,39 @@ describe('DirectProject 回合状态派生', () => {
|
||||
expect(status.pendingCount).toBe(2);
|
||||
});
|
||||
|
||||
it('canStopTurn 只认"有作用对象"的两种状态:队列非空不算', () => {
|
||||
// 队列非空仍然是 displayBusy(界面不假装空闲),但它不是终止的作用对象——终止钮不该出现,
|
||||
// 否则点了只会报「当前没有正在运行的回合」。
|
||||
const queuedOnly = deriveDirectProjectTurnStatus({
|
||||
turnRunning: false,
|
||||
commandInFlight: false,
|
||||
pendingTurns: [pending('client-1')],
|
||||
turns: [],
|
||||
});
|
||||
expect(queuedOnly.displayBusy).toBe(true);
|
||||
expect(queuedOnly.canStopTurn).toBe(false);
|
||||
|
||||
// 宿主已经放行 / 命令还在飞:这两段里有东西可终止。
|
||||
expect(
|
||||
canStopDirectProjectTurn({ turnRunning: true, commandInFlight: false }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canStopDirectProjectTurn({ turnRunning: false, commandInFlight: true }),
|
||||
).toBe(true);
|
||||
// 真空闲:终止钮与发送钮各自归位。
|
||||
expect(
|
||||
canStopDirectProjectTurn({ turnRunning: false, commandInFlight: false }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
deriveDirectProjectTurnStatus({
|
||||
turnRunning: false,
|
||||
commandInFlight: true,
|
||||
pendingTurns: [],
|
||||
turns: [],
|
||||
}).canStopTurn,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('latestTurnState 取最新一轮的两态;没有回合时为 null', () => {
|
||||
expect(
|
||||
deriveDirectProjectTurnStatus({
|
||||
|
||||
Reference in New Issue
Block a user