feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
Cheng Zi Feng
2026-04-17 16:21:27 +08:00
committed by GitHub
parent b5c299f5d2
commit 72a2093cd6
64 changed files with 4138 additions and 312 deletions

View 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)
})
})

View 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,
},
])
})
})

View 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"]);
});
});