feat: add bounded session and history navigation

Add session scrolling, latest-message navigation and fingerprint location under the shared UI gate; reuse navigation in history reads and provide read-only smoke. Navigation index snapshot passed 124 tests and a full build. Windows 4.1.13.63 read-only validation passed 10/10, including two-scroll history location; no sends. Record scope limits, including next-unread navigation not yet implemented.
This commit is contained in:
2026-09-06 20:09:00 +08:00
parent c49f6a10b2
commit e5548bd471
13 changed files with 563 additions and 24 deletions
+66 -10
View File
@@ -74,8 +74,13 @@ try
var output = GetOption(args, "--output") ?? Path.Combine("artifacts", "ui-tree.json");
var report = WechatDoctor.Run(cancellationToken);
var snapshot = await WechatUiInspector.CaptureAsync(output, cancellationToken);
var marker = $"wxagent-smoke-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}";
var confirmed = await WechatChatClient.SendTextAsync(marker, cancellationToken);
var readOnly = HasOption(args, "--read-only");
ChatMessageSnapshot? confirmed = null;
if (!readOnly && report.Errors.Count == 0)
{
var marker = $"wxagent-smoke-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}";
confirmed = await WechatChatClient.SendTextAsync(marker, cancellationToken);
}
WriteJson(new
{
scope = "M1",
@@ -83,9 +88,10 @@ try
output = Path.GetFullPath(output),
nodes = CountNodes(snapshot),
sanitized = true,
sentTo = "file-transfer-assistant",
confirmed.Fingerprint,
confirmed = true
sentTo = confirmed is null ? null : "file-transfer-assistant",
Fingerprint = confirmed?.Fingerprint,
sendSkipped = readOnly,
confirmed = confirmed is not null
});
return report.Errors.Count == 0 ? 0 : 2;
}
@@ -321,10 +327,32 @@ try
var includeContent = HasOption(args, "--include-content");
var limit = GetPositiveIntOption(args, "--limit", 100, 1000);
var scrolls = GetPositiveIntOption(args, "--scrolls", 10, 100, allowZero: true);
var messages = await WechatChatClient.ReadHistoryAsync(limit, scrolls, cancellationToken);
var messages = await WechatChatClient.ReadHistoryAsync(limit, scrolls, cancellationToken,
GetOption(args, "--session") ?? WechatLocators.FileTransferAssistant);
WriteJson(new { messages = messages.Select(message => ToMessageOutput(message, includeContent)) });
return 0;
}
case "chat" when args[1] == "latest":
{
var messages = await WechatChatClient.GoToLatestAsync(GetOption(args, "--session") ?? WechatLocators.FileTransferAssistant,
GetPositiveIntOption(args, "--scrolls", 100, 100), cancellationToken);
WriteJson(new { messages = messages.Select(message => ToMessageOutput(message, HasOption(args, "--include-content"))) });
return 0;
}
case "chat" when args[1] == "merged-open":
{
var windowHandle = await WechatChatClient.OpenMergedChatAsync(GetOption(args, "--session") ?? WechatLocators.FileTransferAssistant,
GetRequiredOption(args, "--fingerprint"), cancellationToken);
WriteJson(new { windowHandle });
return 0;
}
case "chat" when args[1] == "locate":
{
var result = await WechatChatClient.LocateMessageAsync(GetOption(args, "--session") ?? WechatLocators.FileTransferAssistant,
GetRequiredOption(args, "--fingerprint"), GetPositiveIntOption(args, "--scrolls", 30, 100), cancellationToken);
WriteJson(new { result.Scrolls, message = ToMessageOutput(result.Message, HasOption(args, "--include-content")) });
return 0;
}
case "chat" when args[1] == "monitor":
{
var includeContent = HasOption(args, "--include-content");
@@ -359,6 +387,18 @@ try
WriteJson(new { sessions = await WechatChatClient.ListVisibleSessionsAsync(cancellationToken) });
return 0;
}
case "session" when args[1] == "scroll":
{
var direction = GetRequiredOption(args, "--direction") switch
{
"up" => WechatScrollDirection.Up,
"down" => WechatScrollDirection.Down,
_ => throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Direction must be up or down.")
};
var result = await WechatChatClient.ScrollSessionsAsync(direction, GetPositiveIntOption(args, "--pages", 1, 100), cancellationToken);
WriteJson(new { result.Scrolls, result.ViewportChanged, sessions = result.Sessions.Select(session => new { name = HasOption(args, "--include-content") ? session.Name : null, nameId = MaskIdentifier(session.Name), session.IsCurrent }) });
return 0;
}
case "session" when args[1] == "search":
{
var query = GetRequiredOption(args, "--query");
@@ -620,7 +660,7 @@ static int ValidateCommandLine(string[] values)
if (values[0] == "smoke")
{
ValidateOptions(values, 1, ["--output", "--timeout"], []);
ValidateOptions(values, 1, ["--output", "--timeout"], ["--read-only"]);
return 30;
}
@@ -693,7 +733,16 @@ static int ValidateCommandLine(string[] values)
ValidateOptions(values, 2, ["--limit", "--timeout"], ["--include-content"]);
return 30;
case "history":
ValidateOptions(values, 2, ["--limit", "--scrolls", "--timeout"], ["--include-content"]);
ValidateOptions(values, 2, ["--session", "--limit", "--scrolls", "--timeout"], ["--include-content"]);
return 60;
case "latest":
ValidateOptions(values, 2, ["--session", "--scrolls", "--timeout"], ["--include-content"]);
return 60;
case "merged-open":
ValidateOptions(values, 2, ["--session", "--fingerprint", "--timeout"], []);
return 60;
case "locate":
ValidateOptions(values, 2, ["--session", "--fingerprint", "--scrolls", "--timeout"], ["--include-content"]);
return 60;
case "monitor":
ValidateOptions(values, 2, ["--seconds", "--state-file", "--session", "--timeout"], ["--include-content", "--independent"]);
@@ -714,6 +763,9 @@ static int ValidateCommandLine(string[] values)
case "current":
ValidateOptions(values, 2, ["--timeout"], []);
return 30;
case "scroll":
ValidateOptions(values, 2, ["--direction", "--pages", "--timeout"], ["--include-content"]);
return 60;
case "search":
ValidateOptions(values, 2, ["--query", "--ui-output", "--timeout"], ["--exact"]);
return 30;
@@ -863,7 +915,7 @@ WxAgent.Host commands:
doctor [--timeout 30]
diagnose --output <dir> [--baseline <ui-tree.json>] [--timeout 60]
inspect-ui --output <path> [--window-title <title>] [--timeout 30]
smoke [--output <path>] [--timeout 30]
smoke [--output <path>] [--read-only] [--timeout 30]
m4-smoke [--timeout 60]
m5-smoke [--timeout 60]
tray-status [--timeout 30]
@@ -883,10 +935,14 @@ WxAgent.Host commands:
chat send-image --path <image> [--session <name>] [--timeout 60]
chat mention --session <name> --member <display-name> --text <text> --confirm CONFIRM [--timeout 60]
chat read [--limit 20] [--include-content] [--timeout 30]
chat history [--limit 100] [--scrolls 10] [--include-content] [--timeout 60]
chat history [--session <name>] [--limit 100] [--scrolls 10] [--include-content] [--timeout 60]
chat latest [--session <name>] [--scrolls 100] [--include-content] [--timeout 60]
chat merged-open --fingerprint <visible-message-fingerprint> [--session <name>] [--timeout 60]
chat locate --fingerprint <visible-message-fingerprint> [--session <name>] [--scrolls 30] [--include-content] [--timeout 60]
chat monitor [--seconds 60] [--session <name>] [--independent] [--state-file <path>] [--include-content] [--timeout <seconds>]
chat listen [--seconds 30] [--include-content] [--timeout <seconds>]
session list [--timeout 30]
session scroll --direction <up|down> [--pages 1] [--include-content] [--timeout 60]
session search --query <text> [--exact] [--ui-output <path>] [--timeout 30]
session current [--timeout 30]
session open --name <session-name> [--query <keyword>] [--timeout 30]