mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
30
src/utils/__tests__/messageQueueManager.test.ts
Normal file
30
src/utils/__tests__/messageQueueManager.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { isSlashCommand } from '../messageQueueManager.js'
|
||||
|
||||
describe('messageQueueManager.isSlashCommand', () => {
|
||||
test('treats normal slash commands as slash commands', () => {
|
||||
expect(isSlashCommand({ value: '/help', mode: 'prompt' } as any)).toBe(true)
|
||||
})
|
||||
|
||||
test('keeps remote bridge slash commands slash-routed when bridgeOrigin is set', () => {
|
||||
expect(
|
||||
isSlashCommand({
|
||||
value: '/proactive',
|
||||
mode: 'prompt',
|
||||
skipSlashCommands: true,
|
||||
bridgeOrigin: true,
|
||||
} as any),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('keeps skipSlashCommands text-only when bridgeOrigin is absent', () => {
|
||||
expect(
|
||||
isSlashCommand({
|
||||
value: '/proactive',
|
||||
mode: 'prompt',
|
||||
skipSlashCommands: true,
|
||||
} as any),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
174
src/utils/__tests__/sessionState.test.ts
Normal file
174
src/utils/__tests__/sessionState.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
notifyAutomationStateChanged,
|
||||
notifySessionStateChanged,
|
||||
notifySessionMetadataChanged,
|
||||
resetSessionStateForTests,
|
||||
setSessionMetadataChangedListener,
|
||||
} from '../sessionState'
|
||||
|
||||
describe('sessionState metadata replay', () => {
|
||||
beforeEach(() => {
|
||||
resetSessionStateForTests()
|
||||
})
|
||||
|
||||
test('replays cached automation state to listeners that attach later', () => {
|
||||
const seen: Array<Record<string, unknown>> = []
|
||||
|
||||
notifyAutomationStateChanged({
|
||||
enabled: true,
|
||||
phase: 'standby',
|
||||
next_tick_at: 123,
|
||||
sleep_until: null,
|
||||
})
|
||||
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
seen.push(metadata as Record<string, unknown>)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
expect(seen).toEqual([
|
||||
{
|
||||
automation_state: {
|
||||
enabled: true,
|
||||
phase: 'standby',
|
||||
next_tick_at: 123,
|
||||
sleep_until: null,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('dedupes identical automation states after replay but forwards changes', () => {
|
||||
const seen: Array<Record<string, unknown>> = []
|
||||
|
||||
notifyAutomationStateChanged({
|
||||
enabled: true,
|
||||
phase: 'standby',
|
||||
next_tick_at: 123,
|
||||
sleep_until: null,
|
||||
})
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
seen.push(metadata as Record<string, unknown>)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
notifyAutomationStateChanged({
|
||||
enabled: true,
|
||||
phase: 'standby',
|
||||
next_tick_at: 123,
|
||||
sleep_until: null,
|
||||
})
|
||||
notifyAutomationStateChanged({
|
||||
enabled: true,
|
||||
phase: 'sleeping',
|
||||
next_tick_at: null,
|
||||
sleep_until: 456,
|
||||
})
|
||||
|
||||
expect(seen).toEqual([
|
||||
{
|
||||
automation_state: {
|
||||
enabled: true,
|
||||
phase: 'standby',
|
||||
next_tick_at: 123,
|
||||
sleep_until: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
automation_state: {
|
||||
enabled: true,
|
||||
phase: 'sleeping',
|
||||
next_tick_at: null,
|
||||
sleep_until: 456,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('replays merged metadata snapshots instead of only the latest delta', () => {
|
||||
const seen: Array<Record<string, unknown>> = []
|
||||
|
||||
notifySessionMetadataChanged({ model: 'claude-sonnet-4-6' })
|
||||
notifyAutomationStateChanged({
|
||||
enabled: true,
|
||||
phase: 'sleeping',
|
||||
next_tick_at: null,
|
||||
sleep_until: 456,
|
||||
})
|
||||
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
seen.push(metadata as Record<string, unknown>)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
expect(seen).toEqual([
|
||||
{
|
||||
model: 'claude-sonnet-4-6',
|
||||
automation_state: {
|
||||
enabled: true,
|
||||
phase: 'sleeping',
|
||||
next_tick_at: null,
|
||||
sleep_until: 456,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('replays pending_action metadata cached through session-state transitions', () => {
|
||||
const seen: Array<Record<string, unknown>> = []
|
||||
|
||||
notifySessionStateChanged('requires_action', {
|
||||
tool_name: 'Edit',
|
||||
action_description: 'Edit src/utils/sessionState.ts',
|
||||
tool_use_id: 'toolu_123',
|
||||
request_id: 'req_123',
|
||||
input: { path: 'src/utils/sessionState.ts' },
|
||||
})
|
||||
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
seen.push(metadata as Record<string, unknown>)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
expect(seen).toEqual([
|
||||
{
|
||||
pending_action: {
|
||||
tool_name: 'Edit',
|
||||
action_description: 'Edit src/utils/sessionState.ts',
|
||||
tool_use_id: 'toolu_123',
|
||||
request_id: 'req_123',
|
||||
input: { path: 'src/utils/sessionState.ts' },
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('replays cleared task_summary metadata after returning to idle', () => {
|
||||
const seen: Array<Record<string, unknown>> = []
|
||||
|
||||
notifySessionMetadataChanged({ task_summary: 'Running regression suite' })
|
||||
notifySessionStateChanged('idle')
|
||||
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
seen.push(metadata as Record<string, unknown>)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
expect(seen).toEqual([
|
||||
{
|
||||
task_summary: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
84
src/utils/__tests__/taskStateMessage.test.ts
Normal file
84
src/utils/__tests__/taskStateMessage.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
buildTaskStateMessage,
|
||||
getTaskStateSnapshotKey,
|
||||
} from "../taskStateMessage";
|
||||
|
||||
describe("buildTaskStateMessage", () => {
|
||||
test("filters internal tasks and preserves public task fields", () => {
|
||||
const message = buildTaskStateMessage("tasklist", [
|
||||
{
|
||||
id: "1",
|
||||
subject: "Visible task",
|
||||
description: "Shown in web UI",
|
||||
activeForm: "Doing visible task",
|
||||
status: "in_progress",
|
||||
owner: "agent-1",
|
||||
blocks: ["2"],
|
||||
blockedBy: [],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
subject: "Internal task",
|
||||
description: "Hidden from web UI",
|
||||
status: "pending",
|
||||
blocks: [],
|
||||
blockedBy: [],
|
||||
metadata: { _internal: true },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(message.type).toBe("task_state");
|
||||
expect(message.task_list_id).toBe("tasklist");
|
||||
expect(message.uuid).toEqual(expect.any(String));
|
||||
expect(message.tasks).toEqual([
|
||||
{
|
||||
id: "1",
|
||||
subject: "Visible task",
|
||||
description: "Shown in web UI",
|
||||
activeForm: "Doing visible task",
|
||||
status: "in_progress",
|
||||
owner: "agent-1",
|
||||
blocks: ["2"],
|
||||
blockedBy: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds a stable snapshot key for equivalent public tasks", () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: "2",
|
||||
subject: "Second",
|
||||
description: "Second task",
|
||||
status: "pending",
|
||||
blocks: [],
|
||||
blockedBy: [],
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
subject: "First",
|
||||
description: "First task",
|
||||
status: "in_progress",
|
||||
blocks: ["2"],
|
||||
blockedBy: [],
|
||||
},
|
||||
{
|
||||
id: "internal",
|
||||
subject: "Internal task",
|
||||
description: "Hidden",
|
||||
status: "pending",
|
||||
blocks: [],
|
||||
blockedBy: [],
|
||||
metadata: { _internal: true },
|
||||
},
|
||||
];
|
||||
|
||||
const firstKey = getTaskStateSnapshotKey("tasklist", tasks as any);
|
||||
const secondKey = getTaskStateSnapshotKey("tasklist", [...tasks].reverse() as any);
|
||||
const message = buildTaskStateMessage("tasklist", tasks as any);
|
||||
|
||||
expect(firstKey).toBe(secondKey);
|
||||
expect(message.tasks.map(task => task.id)).toEqual(["1", "2"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user