Repository navigation
Stabilize KeylessAgent CLI - #1
Merged
Merged
Conversation
… tooling - KeylessAgent.slnx with Abstractions, Core, Cli, Playground, and Tests projects - Directory.Build.props (net10.0, analyzers, package metadata) and Directory.Packages.props (CPM) - All NuGet packages installed via dotnet CLI (ImanSoftware.* set, Playwright, Spectre, xunit, FluentAssertions 7.x) - nuget.config with package source mapping, publish.ps1, build-and-test workflow - .editorconfig, .gitattributes, .gitignore, global.json, LICENSE.txt, tool icon
- CommandKind with Description attribute per AgentScript v1 command name - CommandStatus, ApprovalMode, SnapshotMode, and AgentEventType enums
- Line-based state-machine parser: all 10 commands, case-insensitive keywords, multiline payloads, nonce markers (<<<CONTENT:x7), code-fence stripping, line-numbered problems for every grammar violation - AgentScriptValidator: response/file-content size limits and path shape rules - ResponseFormatGuard: single gate producing FormatGuardResult (valid script or problems) - ErrorCodes catalog (KA_*), configuration options model, format-correction.txt template - 28 xunit tests covering all parser and guard paths
…ckups - GitService via short-lived git processes (info, branch, stash, stage, commit, push -u, log, .git/info/exclude upkeep); never force or reset - BranchGuard enforcing protected branch patterns (main/master/release*) in code - BranchNameBuilder: agent/<yyyyMMdd-HHmm>-<slug> with request-derived kebab slug - PathSandbox: workspace-relative only, no .. or absolute paths, .git/.agent blocked, symlink escape detection - CommandPolicy: deny list (format/shutdown/force-push/global-config/pipe-to-shell) plus configurable allow list for AutoSafe - Per-command executors (say/newfile/updatefile/editfile/deletefile/readfile/ runcommand/ask/commit/done) behind WorkspaceCommandExecutor with dry-run, Ask-mode diffs/confirmations, BOM preservation, and backup-before-change - BackupService into .agent/backups with undo-latest restore
- Tests for path sandbox (.., absolute, .git/.agent, junction escape), command policy (deny list, segments, allowlist), branch guard (main/MASTER/release*/releases vs working branches), branch name builder (fake clock, empty/non-Latin/symbol/long slugs), and all executors (new/update/edit/delete/read/run/ask/commit incl. BOM preservation, backups, dry-run, declines, push failure, non-zero exits) - Fix sandbox: symlink check no longer rejects paths whose parent folders do not exist yet - Slug: whitespace collapses to dashes, repeated dashes collapse - Relax analyzers for the test project (not shipped)
- SnapshotService runs tree-view.bat/code-dump.bat via cmd /c without a window, stdin closed and hard timeout so the trailing pause can never hang; staleness check ignores the snapshot outputs themselves - SnapshotFilter: tree lines filtered by folder name including nested blocks; dump sections dropped by heading; chunking only at ### section boundaries - ResultFormatter builds [KEYLESSAGENT RESULTS] messages mapping every result to command number/name/target with head+tail output capping and the skipped-commands note - Tests: filter/chunk, bat-run-without-hanging, staleness, attachments, formatter ids/truncation/skipped
…extractor, fake - PlaywrightChatProvider: ConnectOverCDP to the user's Chrome (default) with LaunchPersistentContext fallback, manual-login wait, new-chat click, and DeepThink/Search toggle enforcement via aria-pressed (warn and continue when a toggle cannot be found) - PlaywrightChatSession: rate-limited sends, whole-text insert (never per-character typing), attachment upload via file input with paste fallback, reply waiting on stop-indicator + stable final-answer read, debug screenshots - Default selectors.json built from the real DOM sample (structural selectors over hashed classes) with fallback lists, editable without recompiling, auto-written to the workspace - ReplyExtractor: tag-scanner returning only the final answer, skipping .ds-think-content and code-block banners, preserving pre indentation - FakeChatProvider (scripted replies) + local RateLimiter - Playground harness running one scripted conversation end to end - 139 tests green; solution builds with zero warnings
- AgentLoop orchestrating one turn in numbered steps: config load, git preconditions (repo check, dirty-tree continue/stash/cancel), protected- branch guard with agent-branch creation, session resume/new chat, stale-snapshot refresh, first message with system prompt + attachments, the conversation loop (wait -> guard -> execute with batch limits and skipped reporting -> results message), and an always-on snapshot refresh in finally - Automatic format re-ask with correction template up to MaxFormatRetries, raw reply shown to the user when exhausted - ConfigurationService: tolerant keyless.json (comments, case, trailing commas, string enums), default file written when missing, range validation - SessionStore persisting chat URL/branch/turn in .agent/session.json (no-session is a KA_SESSION_NOT_FOUND failure, matching the Outcome package's rule that Data is never null) - SystemPromptBuilder injecting limits into the embedded system-prompt.md - KeylessAgentServiceFactory wiring everything by hand (no DI container) - TurnLogger JSON-lines transcript, FileLoggerProvider for debug diagnostics - 156 tests green including end-to-end loop tests with the fake provider
…ckaging - SpectreAgentUserInterface: say messages as panels, progress through the status spinner, prompts for confirm/ask, colored +/- diffs before changes - Commands: 'run <request>' (--dry-run, -v), interactive default command (prompt-per-turn session), 'resume <request>' (reuses .agent/session.json chat), 'undo' (restores newest backup batch), 'config' (prints effective keyless.json, writing defaults when missing) - AgentCliRunner: shared spinner + branch/commit summary at turn end - KeylessAgentServiceFactory.CreateSessionStore/CreateFileLoggerFactory; SingleProviderLoggerFactory for verbose diagnostics logging - publish.bat calling the root publish.ps1; tool verified to pack (nupkg+snupkg)
- Full README (what/why, requirements, install, usage, Mermaid agent-loop flowchart, AgentScript summary, safety model, known limitations, roadmap incl. WinForms UI and official-API provider, ToS/account-risk notice, not-affiliated statement) and the short tool README - docs/: architecture.md, agentscript-spec.md (grammar, commands, limits, full KA_ error-code catalog), configuration.md, browser-automation.md (selectors table + manual verification checklist), security.md, git-workflow.md - Checklist review: inject IDateTimeProvider into the browser classes so Core has no direct DateTime.Now/UtcNow; verified zero warnings, zero csproj package versions, exactly six ImanSoftware packages, no Spectre/console usage in Core — 156 tests green
…nt directory - All commands now take the workspace path as their first positional argument: 'keyless run <workspace> <request>', 'resume <workspace> <request>', 'undo <workspace>', 'config <workspace>' - When omitted, the very first prompt asks for it, validating that the directory exists (current directory shown only as the default, never silently assumed); run/resume also prompt for a missing request - Interactive default command prompts for the workspace before the first request; -w removed in favor of the positional argument - README (root + tool) usage examples updated to workspace-first form
…-fatal Bug: wrapping a whole agent turn in AnsiConsole.Status made every Say/ Confirm/Ask during the turn throw Spectre's 'Trying to run one or more interactive functions concurrently' and kill the process with exit 1. - IAgentUserInterface gains BeginWait(message): Core now marks only actual waiting phases (chat reply, sends, runcommand process, snapshot scripts) with a disposable wait scope — never the whole turn - New ConsoleUserInterface owns all console access through one semaphore; the spinner is a background task cancelled+joined before ANY other UI call (Say/Ask/Confirm/ShowDiff/ShowProgress), overlapping BeginWait is safe (previous spinner stopped first), scopes tolerate double stops - AgentCliRunner no longer wraps turns in Status; a failing or cancelled turn is printed (message + .agent/logs pointer) instead of crashing - InteractiveSession loop: turn failures return to the keyless> prompt, Ctrl+C cancels only the current turn (fresh linked token per turn); exit code 1 reserved for startup errors - Console Output/InputEncoding set to UTF-8 at startup when not redirected; console markers switched to plain ASCII ([ok] [!] [x] [i]) with proper markup escaping - Tests: nested-exclusivity reproduction of the crash, BeginWait+Confirm stops spinner first, Say during a wait, overlapping waits, Ask after a disposed wait, and failing/cancelled turns returning to the prompt (Spectre.Console.Testing 0.57.2 via CPM); 164 tests green
Before every send the newest assistant message is fingerprinted (data-virtual-list-item-key, else a hash of the final-answer text, plus the visible count); the send itself must be confirmed within Browser.SendConfirmSeconds (new, default 15: new message item, or empty input + generating indicator) with one retry (click then Enter) before failing KA_SEND_NOT_CONFIRMED; WaitForReplyAsync accepts only a reply whose fingerprint differs from the pre-send one (identical text valid only with a new message key), whose final answer exists and is stable for two reads, and once the stop indicator is gone. Attachment sends wait for chips + enabled send (Browser.UploadWaitSeconds, default 60) and fall back to pasting on timeout. Session logic now sits behind the IChatPage seam (PlaywrightChatPage = real DOM; FakeChatPage in tests), enabling scenario tests (a)-(d). Docs updated; 168 tests green.
- LineEndings helper: detect dominant ending (mixed keeps the majority, tie means LF), normalize to LF, apply back; new files resolve their ending from .editorconfig (end_of_line, section globs), then .gitattributes (eol=), then the dominant ending of sibling source files (dot/config files excluded), then Environment.NewLine - editfile: match and count FIND/REPLACE on LF-normalized text (the 'matched N times' message included) and write back with the file's own ending; updatefile converts model content to the existing ending; newfile applies the resolved ending; BOM handling unchanged - FileChange diffs are LF-normalized (no stray \r in the CLI diff) and MaxCommandOutputChars counts normalized characters in result messages - Tests: CRLF+LF FIND matches once and stays CRLF, multi-line find, mixed-ending file, BOM+CRLF, newfile obeys .editorconfig and Windows NewLine, updatefile keeps endings, editorconfig globs, gitattributes, sibling dominance, diff without \r; 185 tests green
- deletefile now asks for confirmation in Ask AND AutoSafe modes; only Auto skips it (a backup is always made in every mode) - AutoSafe/Auto turns show a one-time warning at turn start: 'AutoSafe is not a security boundary' (dotnet/node/python can run arbitrary code) - docs/configuration.md, docs/security.md and the README approval table now state exactly what the code does - Tests: deletefile asked+declined in Ask and AutoSafe, not asked in Auto, and the AutoSafe boundary warning; 186 tests green
- BeginWait returns only after the Status display actually started (ManualResetEventSlim set from inside the display), so a prompt can never start in the gap; StopSpinner no longer gives up after 3s — it joins fully (30s upper bound with a warning), so any prompt/write always runs after the spinner is really gone - ConsoleWriter becomes an instance over one IAnsiConsole (Shared for command output); commands and the runner route everything through the same console surface as the agent UI - Turn failures print exception type + message; with -v the full stack trace is shown and appended to .agent/logs/diagnostics.log; the log path is always printed; WorkspacePrompt uses the same ASCII [x] marker - Tests: prompt immediately after BeginWait never throws, slow spinner shutdown still lets the following prompt run, failing turns show the exception type and return to the keyless> prompt; 190 tests green
- SendButton fallback order per the sample: icon button in the textarea's sibling container, iconLabelPrimary, filled-circle, filled next to the file input, submit — each candidate checked for visibility and aria-disabled before clicking (a disabled send is skipped, never the target); Enter stays the last fallback - Every selector loop logs (Debug) which fallback INDEX matched, so a broken selector is one log line away - Toggle and MessageItems selectors made pure-CSS first (:has-text kept as a Playwright-only later fallback); AttachmentChips added earlier - Fragment tests with AngleSharp run the default catalog against real fragments of deepseek-dom-sample-trimmed.html: input placeholder, file input multiple, toggles aria-pressed, virtualized item keys, final answer vs reasoning, send fallbacks matching and skipping the disabled button; 198 tests green
- /workspace <path> validates the directory, refuses while the loop is not ready (a failing switch keeps the old workspace), re-runs the git preconditions and clears the target workspace's .agent/session.json (fresh chat session), then prints the new header; every turn and callback receives the workspace explicitly — no CurrentDirectory - /help lists the slash commands; any other /-prefixed input is rejected with a hint instead of being sent to the model - InteractiveSession takes the workspace as a parameter and a switchWorkspace callback; InteractiveCommand wires preconditions via the git service and session store - Tests: help listed without a model send, unknown /command rejected, switch recorded + used for subsequent turns, missing directory refused, failed switch keeps the old workspace, usage message; 204 tests green
- Send retry happens only when the input STILL holds the text; a cleared input means the message was sent even when the virtualized item count did not change (treated as sent, then WaitForReplyAsync). Retry exactly once, and each attempt is the send button when clickable, otherwise Enter — never both in the same attempt (first attempt included) - Attachment delivery judged by several signals: SetInputFiles result, the hidden file input's reported file names, composer-near elements showing a file name, chips visibility. Paste only when SetInputFiles itself failed or there is NO sign of the files after Browser.UploadWaitSeconds; uncertainty counts as attached with a warning — the same content is never both attached and pasted - FakeChatPage gains FailClick, InputFileNames, VisibleFileNames and LastInsertedText; tests (a)-(e) cover all five scenarios; 209 tests green
- IChatDiagnostics (Core, GUI-reusable) + ChatDiagnosticsService over the IChatPageInspector seam: probes every selectors.json key through the fallback lists and classifies MATCHED (index + count) / NOT FOUND / NOT APPLICABLE NOW (Stop idle, no chips, no reasoning, fresh chat, login); reports login state, DeepThink/Search aria-pressed, input editability and the page URL; exit 0 only when all required keys matched; never sends a message - PlaywrightChatInspector (real DOM) with the shared connection logic from PlaywrightChatProvider; a new tab, never touching the user's chat - --capture <dir>: trimmed HTML snapshots (composer, newest message, sidebar, chips) via HtmlTrimmer — scripts/styles/svg removed, long text replaced with '...' so no personal content leaves the machine - --watch: waits for a manually sent prompt, captures generating and finished states once each - keyless doctor CLI command with a state table; docs section 'How to verify selectors: keyless doctor'; 5 tests with a scripted inspector; 214 tests green
- All IChatPage operations are Task-returning with a CancellationToken; Playwright's waits have no token support, so each call races the token (WithCancellationAsync) — Ctrl+C interrupts any in-flight wait promptly instead of waiting out its timeout; FakeChatPage and all session tests updated - KA_SEND_NOT_CONFIRMED and KA_REPLY_TIMEOUT errors now carry a pointer: the debug screenshot path under .agent/logs when Browser.Debug is on (the page writes to the exact path), otherwise a hint to run 'keyless doctor'; screenshot path bug fixed (the destination path, not the name, is passed to the page) - docs/browser-automation.md: send/confirm and attachment decision tables, the still-unverified selector list (Stop, New Chat, chips), and the cancellation note - 216 tests green; build with zero warnings
…ibutes - HtmlTrimmer rewritten: removal tags became HTML comments (the old '<script removed>' literal parsed as a real element); attribute values (href/src/title/alt/value/placeholder/aria-label/data-*) are masked to '[masked]' unless they are short allowlisted UI labels; structural attributes (class/role/aria-pressed/aria-disabled/type/accept/multiple) survive; data-virtual-list-item-key is replaced with a stable '[key:hash]' - Text runs longer than the cap no longer swallow the following '<' (old '>...<' bug) — text nodes are masked per-node instead of one greedy run - Allowlist: DeepThink, Search, New chat, Stop, Message DeepSeek, 'Thought for N seconds' pattern, Copy, Download, Regenerate - Tests with AngleSharp: sidebar titles/hrefs, user messages and attachment file names never appear; UI labels + structural attributes survive; trimmed HTML parses to the same element structure (minus the removed script/style/svg); 223 tests green
… readiness - IsSendEnabledAsync is tri-state now: true/false when a send control matched, null when no send-control selector matches at all (fake page and real page agree); a null state never blocks the send - After SetInputFiles succeeds, the session waits (up to Browser.UploadWaitSeconds) for BOTH a positive upload sign (files registered in the input, a chip showing a file name, chips visible) AND an enabled send control, whenever that control can be detected; an upload registered while the control stays disabled is sent anyway with a warning after the timeout - Tests: send enabled only after N polls -> the click happens after enablement; undetectable send control -> no deadlock (returns well before the timeout); 225 tests green
- SelectorsCatalog.LoadAsync, PlaywrightChatInspector (ProbeAsync/ GetToggleStateAsync/IsInputEditableAsync/CaptureTrimmedAsync) and the whole diagnostics pipeline are async with CancellationToken - FileLoggerProvider no longer blocks inside ILogger.Log: Log enqueues into a Channel<string> and one background task performs all storage I/O; the provider constructor does no I/O at all - TurnLogger.StartAsync/AppendAsync/AppendResultAsync; AgentLoop awaits them; /workspace switch flow is async end to end (git preconditions and session clearing happen on the async path with the turn's token) - New NoSyncOverAsyncRuleTests greps all src/ production sources for GetAwaiter().GetResult()/.Result/.Wait() and fails on any violation (tests are exempt); 225 tests green, zero warnings
…tab rules Observed bug: a separate logged-out Chrome window opened (never focused) showing the DeepSeek login page instead of the user's session. Root causes fixed: - Chrome 136 ignores --remote-debugging-port for the DEFAULT user-data-dir: the docs claiming a daily-profile attach were wrong and are corrected everywhere (README requirements, browser-automation Connection section, configuration.md). A dedicated profile (Browser.UserDataDir, default %LOCALAPPDATA%\KeylessAgent\chrome-profile) is now mandatory and the login persists in it - The silent LaunchPersistentContext fallback is removed; it is opt-in (Browser.AllowPlaywrightLaunch=false) with an explicit warning - pages[last] navigation (which could destroy a user's tab) is gone: an existing chat.deepseek.com tab is reused (Browser.ReuseExistingTab), otherwise a new TAB opens via CDP Target.createTarget newWindow:false, matched by URL polling; the tab is focused (BringToFrontAsync + Target.activateTarget), again when login is required - Connection is now IBrowserConnector in Core (probe -> attach; auto-launch the REAL Chrome via IBrowserEnvironment: port probe, chrome.exe locate via config/registry/paths, Process.Start, wait 30s, attach; actionable KA_BROWSER_CONNECTION_FAILED with the exact manual command line; port-open-but-attach-failed reports the reason instead of falling through), shared by run, doctor and the new 'keyless chrome <workspace>' command (dedicated Chrome + one-time login) - Dispose policy: only the tab the agent opened is closed; the Chrome window stays running (Browser.CloseChromeOnExit=false default) - New config keys: UserDataDir, AutoLaunchChrome, ChromePath, ReuseExistingTab, AllowPlaywrightLaunch, CloseChromeOnExit - Tests with faked environment: port open -> attach (no relaunch), port closed -> launch then attach, Chrome missing -> actionable failure, auto-launch off -> fail instead of fall through, Playwright fallback opt-in surfaces as failure without a real browser, profile default resolution; 232 tests green
- IAgentUserInterface.ChooseAsync with ChoiceRequest/ChoiceOption/ChoiceResult (UI-agnostic); ConsoleUserInterface numbered menu: arrows move, digits 1-9 pick immediately, Enter confirms the highlighted (default marked), Esc/Ctrl+C cancels; non-interactive input returns the default and prints it; runs inside the console gate (spinner stopped first) - AgentLoop preconditions rewritten as one combined flow: at most ONE menu per turn, none when the tree is clean and the branch is protected (agent branch auto-created); dirty menu carry/stash/here/cancel (here omitted on protected branches); branch menu new/stay/cancel on clean non-protected branches; the typed 'type continue/stash/cancel' and 'press Enter or type keep' prompts removed - Auto git init (Git.AutoInit default true): git missing -> KA_GIT_NOT_INSTALLED with download link + manual steps; git init -b main (fallback init + branch -M main); .NET .gitignore via dotnet new gitignore or built-in template when the workspace has .sln/.slnx/.csproj; initial commit 'Initial commit (created by KeylessAgent)' with one-shot -c user.name/user.email identity fallback - CommitExecutor: no 'origin' remote -> commit locally and skip push with a notice in the result sent to the model; origin present + push failure reported as before; KA_GIT_INIT_FAILED for init/commit failures; AgentTurnResult.ErrorCode - ConfirmAsync shows the bracketed default, Enter accepts, no empty-input loop - 233 tests green
…back
- Browser.ReplySource = Auto | Copy | Dom (default Auto); Auto tries the
Copy capture, validates, falls back to Dom; Copy never falls back
silently (KA_REPLY_TIMEOUT); Dom is the original behavior
- New selector key MessageCopyButton (message-level Copy copies ONLY the
final answer; bar may be hover-only — the newest message is hovered
first, and only the NEWEST message's control is ever clicked)
- IChatPage capture members + Playwright implementation: ExposeBinding
interception wrapping navigator.clipboard.writeText, execCommand('copy')
and the copy event, gated on an agent-only flag (user's manual copying
and the OS clipboard are never affected); clipboard fallback grants
clipboard-read/write, saves the previous text, reads readText() and
ALWAYS restores it in a finally block
- ReplyReader: strategy + validation (normalized markdown/punctuation-
stripped comparison: containment or similarity >= 0.9; reasoning start
must be absent; copy >= 50% of DOM length; numbers logged on rejection)
- Completion detection unchanged (fingerprint + final answer + stop +
stable); IChatSession.LastReplyOrigin surfaces Copy/Dom/Dom (copy
rejected) and the AgentLoop logs reply-source per reply
- Parser fence tolerance: outer fence stripped only when the first AND
last non-blank lines are fences of the same or greater length (3 or 4
backticks); inner fences inside CONTENT payloads untouched; system
prompt tells DeepSeek not to wrap replies in a code fence
- doctor: MessageCopyButton probe + ReplyCapture check (strategy,
clipboardUntouched, copiedLength, validation result)
- 247 tests green (14 new: ReplyReader validation/strategies, parser
fences, interception/clipboard/none flows)
…ODO removed
- README rewritten: tagline, requirements (.NET 10, Windows, git, Chrome with
the Chrome 136 dedicated-profile rule), install, first run, usage with the
numbered git menus and auto-init notice, Mermaid agent loop, AgentScript
summary, reply sources table (Copy/Dom), safety model ('AutoSafe is not a
security boundary'), known limitations, roadmap, layout, ToS/not-affiliated
- docs: git-workflow.md (auto init, numbered menus, at-most-one-menu flow,
no-origin push skip), security.md (menu note), configuration.md
(ReplySource + Git.AutoInit), browser-automation.md (reply sources,
capture strategies, validation rules), agentscript-spec.md
(KA_GIT_NOT_INSTALLED, KA_GIT_INIT_FAILED)
- CLI README updated (chrome/doctor commands, menus, auto init)
- DocsConsistencyTests: every ErrorCodes KA_ code must appear in
agentscript-spec.md; every Browser/Limits/Git/Commands default must appear
in configuration.md; the documented keyless.json block must deserialize to
values equal to the default options object
- DOCS-TODO.md deleted; 250 tests green
MoveFileExecutor mutated silently: no diff, no confirmation — unlike
deletion. After the sandbox/existence checks and before the backup, Ask
and AutoSafe modes now show a FileChange diff for the move and ask for
confirmation ('Move file', with the destination and the backup note);
a decline returns KA_COMMAND_DECLINED and nothing is moved. Auto keeps
running without asking, and dry-run still previews without prompting.
Tests: declined move leaves both files untouched with KA_COMMAND_DECLINED;
confirmed move relocates the file and records exactly one diff. The test
suite's executor registry gained the MoveFileExecutor it was missing
(which is why the move path had never been exercised end-to-end there).
454 passing.
keyless undo only restored backed-up files: a @@newfile creation survived undo and a @@movefile left its destination behind. Each backup batch now carries a journal.jsonl - {op:create,path} written by NewFileExecutor after a successful create, {op:move,from,to} written by MoveFileExecutor into the same batch that holds the backed-up source (BackupAsync was split into BackupToBatchAsync returning the stamp plus an AppendJournalAsync; both best-effort - a journal failure never fails the command). UndoLatestAsync processes the journal first (delete the created file, delete the move destination, reported as 'removed: <path>' entries) and then runs the existing copy restore, skipping the journal file itself. Tests drive the real executors in Auto mode: undo deletes an agent-created file; undo restores the moved source and removes the destination. 456 passing.
FirstVisibleAsync raced fallback selectors SEQUENTIALLY with the full timeout each, so an absent element cost N x timeout: login probing (3 x 5s = 15s per start), stop-indicator polls (3 x 400ms per reply poll). The fallback list now races in ONE combined Or-locator with ONE timeout (winner identified by instant probes afterwards), and a new instant IsAnyVisibleAsync (single round trip, no waiting) answers 'is it there right now' - stop-indicator polling, chat-limit notices, attachment chips, toggles and send-enabled all use it; the session's default reply poll drops to 100 ms. Login detection is positive-first: ONE wait races InputBox against LoginIndicator (Browser.LoginProbeSeconds, default 15) via IChatPage.WaitForAnyVisibleAsync; only a winning login indicator focuses the tab (IChatPage.FocusTabAsync) and waits for the manual login. The minimize call moved to the provider caller so the probe method is pure. Per-phase timings now cover login-probe, toggles, upload, paste, send, send-confirm, first-token, completion and copy (ChatSessionOptions.Timings; the loop adds parse and passes its recorder into the session), plus a compact 'Reply: N commands' progress line for valid replies. 456 passing.
PlaywrightChatProviderTests: the positive-first login probe with a fake page - a logged-in start costs ONE parallel wait and never focuses the tab or runs the login loop; a winning login indicator focuses once and then polls with instant checks (never per-fallback waits); an inconclusive probe (nothing matched) is treated as logged in. FakeChatPage gains round-trip counters (stop polls, chat-limit probes, any-visible, parallel waits, focus) and scripted results. PlaywrightChatSessionTests: the stop-indicator poll interval stays at most 100 ms (polls >= elapsed/ 100ms with instant checks, no serialized timeouts) and the chat-limit probe costs exactly one round trip per reply wait. 461 passing.
Ctrl+C was only handled during keyless run; the interactive session died with the process default. The session now owns Console.CancelKeyPress via one RequestCancel seam: at the idle prompt a press exits gracefully (exit 0; per-turn log flushes and connector disposal already happen at turn end), during a turn the first press cancels only the turn and a second press within 2 seconds exits gracefully. The idle prompt races the exit signal (WhenAny) so the exit does not wait for stdin. Tests: idle cancel exits 0; first cancel during a turn cancels exactly the turn, second cancel within 2s exits 0. 461 passing.
ShowProgress printed raw enum names ('ValidatingReply ...'); it now
renders one consistent '› step: detail' shape with stable lowercase step
labels per event type, and a test pins the compact 'Reply: N commands'
line for valid replies. 463 passing.
browser-automation.md: the positive-first login probe (one parallel InputBox-vs-LoginIndicator wait, LoginProbeSeconds=15, focus only when the user must act), parallel Or-locator waits and instant checks with the ~100 ms reply-poll cadence, the ChatLimitNotice selector row, and the interactive Ctrl+C behavior. configuration.md: LoginProbeSeconds key, login-probe/fast-wait/--timing paragraphs. README + CLI README: --timing, /newchat, --verbose, Ctrl+C semantics. DOCS-TODO.md deleted (it was empty). DocsConsistencyTests extended: configuration keys, the recoverable KA_ codes, README CLI options and slash commands, CLI README slash commands and Ctrl+C. 467 passing.
A PlaywrightChatSession test with a scripted TimingRecorder asserts the phases recorded around the real session flow: toggles, upload, send, send-confirm, first-token, completion and copy all land in the table. 468 passing.
Three wait sites still paid one timeout per fallback: the InputBox wait (10 s x N in InsertTextAsync), the SendButton wait (3 s x N in TryClickSendAsync) and the MessageCopyButton wait (full timeout x N). All three now use the combined Or-locator FirstVisibleAsync helper - one wait, one timeout, the matched fallback index returned; the same total upper bound as today (the per-fallback timeout) applies once. TryClickSendAsync keeps its disabled/paperclip screen, now on the single matched candidate (a disable/paperclip verdict falls back to Enter instead of paying more waits). FirstVisibleAsync gained a scope-based overload (resolve delegate) so the copy-button race runs scoped to the newest message, and the winner probes resolve at the same scope. The clipboard fallback's fixed 500 ms sleep is now a 75 ms poll of the read with the same 500 ms budget. Remaining WaitForAsync in the page: exactly one (the combined wait itself). 468 passing.
…ter visible one The Or-locator's .First is the first match in DOM ORDER - a hidden element listed first made the visible-wait time out even though a later fallback was visible. The combined wait now applies Filter(Visible = true) (supported by the pinned Playwright 1.64.1; the EvaluateAsync fallback is not viable because catalog selectors use Playwright-only pseudo-classes like :has-text), so the race is over visible matches only. Winner selection is extracted into SelectVisibleWinnerAsync - the first selector whose instant visibility probe passes, in fallback order; hidden earlier matches are skipped and nothing visible yields null. FakeChatPage models DOM-order visibility (DomVisibility) for the parallel wait: a provider test proves a hidden first InputBox selector does not stop a visible login indicator from winning (focus happens, login poll runs), and a unit test pins the winner rule itself. 470 passing.
'Choose or open the chat tab' was the one untimed connect step. IBrowserConnector.ConnectAsync gains an optional ITimingRecorder (the provider passes ChatSessionOptions.Timings), and OpenChatTabAsync wraps its whole choose-or-open body (reuse-existing-tab, Target.createTarget, page wait, and the test-seam override) in a 'tab-open' phase - a null recorder costs nothing. Tests: the auto-launch path records tab-open with a recorder and works without one. 472 passing.
NSubstitute joins the test project so the real Playwright IPage/ILocator surface can be stubbed. PlaywrightChatPageRoundTripTests count the Playwright round trips of the fallback waits: an absent InputBox costs exactly one WaitForAsync with the 10s single budget (then the documented no-input exception), an absent SendButton one WaitForAsync with the 3s budget (then false), an absent MessageCopyButton one scoped WaitForAsync with the caller's timeout (then false), the stop-indicator check one IsVisibleAsync round trip, and a hidden-first DOM shows the winner probes walking in fallback order until the later VISIBLE selector and the text being inserted there. 477 passing.
docs/configuration.md documented MinDelayMs 400 / MaxDelayMs 1500 while the code defaults are 0 and 150, and placed Browser.MaxTurnsPerChat under the Git section. Fixed. The new DocsConsistencyTests.DefaultKeylessOptions_SerializeMatchesDocumentedJson parses the documented jsonc blocks (string-aware // stripping), compares them against the SERIALIZED default KeylessOptions with the production serializer in both directions - every documented key must exist with the same value (arrays element-wise, numbers numerically), and every options property must be documented - so default drift can no longer happen silently. 478 passing.
code-dump attachments carried .json/.config files verbatim: connection strings, API keys and whole environment files reached the DeepSeek chat. Snapshot.Filter gains SensitiveFilePatterns (env files, key material, *secret*/*password*/*credential*, production appsettings - case- insensitive wildcards against the file name) and MaskSensitiveLines (default true). BuildAttachmentsAsync now drops dump sections whose file name matches a sensitive pattern and redacts secret-looking values in the remaining sections - Password/Pwd/ApiKey/secret/token/credential assignments keep the key name and become key=[redacted], JSON keys keep their quoted shape, Authorization Bearer/Basic tokens are redacted, PEM private-key blocks are redacted line-wise between BEGIN and END. The tree attachment gets a '[N files filtered and M values masked as sensitive]' marker line (omitted when both are 0). The attachment copy only: code-dump-result.md, tree-view-result.txt and every source file on disk stay byte-identical. New keys documented in configuration.md (the consistency test caught them immediately) with a DOCS-TODO bullet. Tests: .env/secrets.json/appsettings.Production.json filtered with marker, secret values masked with keys kept, disk outputs unchanged, and a comment mentioning 'password' untouched. 481 passing.
After 'Port N is open but attach failed' the connector logged and showed progress but fell through to the auto-launch step - starting a SECOND Chrome on a port already owned by another Chrome, which could only fail again. The comment said 'do not fall through'; the code now honors it: the connect fails right there with KA_BROWSER_CONNECTION_FAILED, the attach error, and the hint 'Close the Chrome that owns port N or change Browser.ChromeDebugPort.' FakeEnvironment gains FailAttach (attach throws); the regression test asserts the failure code, the hint, and that StartChrome was never called. 482 passing.
RunTurnAsync printed the stopped-reason warning and then returned 0 - CI and scripts could not tell a stopped turn from a successful one. Exit codes are now 0 success / 1 error / 2 stopped early / 130 cancelled; the summary output (commits, summary, change set, timings) still prints for stopped turns since they may have done real work. Also fixed a latent test-flake root: AgentCliRunner used ConsoleWriter.Shared, which binds AnsiConsole.Console at first touch - under the parallel suite the warning could go to another test's captured console; the writer is now resolved per call. Tests: stopped turn returns 2 with the warning, failed turn returns 1, successful turn returns 0; the exit-code table is in DOCS-TODO.md. 485 passing.
SessionStore.LoadAsync deserialized without try/catch - invalid JSON in .agent/session.json threw and killed the turn. It now treats any deserialize failure (or null result) as no-session with KA_SESSION_NOT_FOUND and renames the broken file to session.json.corrupt (best effort) so the content survives for inspection; valid sessions are never renamed. TurnLogger.StartAsync/AppendAsync wrote through storage unguarded - an I/O error would fail the turn. Writes are now wrapped (outcome failure AND exception), and the first failure disables logging for the rest of the turn with exactly one warning through the new optional warning callback; cancellation still propagates. Tests: corrupt session returns KA_SESSION_NOT_FOUND with the .corrupt file preserved and valid sessions never renamed; a logs path blocked by a file disables logging with one warning and no throws; healthy logging stays warning-free. 489 passing.
The batch loop ran EVERY command in the reply: after @@done set completedByDone, the foreach kept executing later blocks (a newfile after @@done really created the file) and the done-check only broke after the whole foreach. Now a SUCCESSFUL @@done reports every remaining non-say command as Skipped with the reason 'after @@done' (executed list + turn log via AppendResultAsync - the results message is intentionally not sent when the turn is complete) and breaks the batch. A failed @@done keeps the old behavior. Test: say + done + newfile leaves x.txt uncreated with a Skipped/after @@done result entry in turn-1.jsonl. 490 passing.
AppendResultAsync serialized CommandExecutionResult verbatim: command output with credentials (connection strings, tokens, printed env vars) landed in .agent/logs/turn-N.jsonl in plain text. New SecretRedactor (compiled, case-insensitive): password/passwd/pwd/secret/token/apikey/ api-key/authorization/bearer followed by : or = and a value becomes '<key>=[redacted]' - optional quotes around key and value keep JSON output structured, an optional bearer/basic scheme word is consumed so 'Authorization: Bearer <token>' loses the whole credential, and the value stops at whitespace/quote/semicolon/comma so 'Password=x;Server=y' only loses the credential. AppendResultAsync redacts the Output FIELD before serialization (never regex over serialized JSON); requests and replies are logged as received. Tests: password= line, JSON ApiKey and Bearer token redacted with structure and non-secret lines kept. 492 passing.
PlaywrightChatInspector.IsInputEditableAsync hardcoded two textarea selectors and paid a 2s wait per fallback. IChatPageInspector .IsInputEditableAsync now takes the fallback list (catalog.InputBox from ChatDiagnosticsService) and probes it with the shared parallel Or-locator helper - one wait with the same 2s-per-fallback total budget. FakeChatPage's inspector records the lists it receives; the regression test asserts the doctor passes exactly catalog.InputBox. Also: the stop-poll interval test's wall-clock upper bound was flaky under parallel-CPU jitter (33 polls on this machine vs a 30 cap) - the discriminating lower bound (instant checks, not serialized timeouts) is kept without the jittery cap. 493 passing.
1. Upload fallback: SetInputFiles success + no upload sign no longer pastes the content on top (it would double it) - treated as uploaded with a warning; paste only when the upload itself failed. 2. CloseChromeOnExit: the dispose lambda now knows the connection kind - a launched Chrome is closed for real (the old pages.Count == 0 guard was almost never true with our own tab open); an attached Chrome is NEVER closed even with the flag set. 3. ProcessRunner: timeout/cancel keep the partial stdout/stderr (readers drained up to 2 s) and expose Reason (timeout/cancelled); the executor reports cancelled commands as KA_CANCELLED, not KA_TIMEOUT. 4. Attachment size report: the UploadingAttachments progress event now carries 'Attachments: N files, X KB' plus the filtered/masked marker. 5. SnapshotFilter invariants exposed a REAL leak: root-level bin/obj folders' children share the folder's marker column, so the depth <= pop consumed the exclusion on the first child. Folder/file lines now follow different pop rules (siblings pop at <=, files drop while covered at >=); nested same-name, case differences and near names pinned by tests. 6. MessageCopyButton reported by keyless doctor (NotApplicableNow when no assistant message is visible). 7. Prompt-injection tests already existed (NOT REPRODUCED). 8. /newchat new-chat-URL test already existed (NOT REPRODUCED). KA_CANCELLED documented in agentscript-spec.md (consistency test caught it). 504 passing.
Program.cs hardcoded SetApplicationVersion("1.0.0") while the version
also lived in the CLI csproj - two sources that could drift. Now:
<Version> lives in Directory.Build.props (inherited by the csproj), the
csproj's own <Version> is removed, Program.cs reports the assembly
version (major.minor.patch, fallback 0.0.0), and publish.ps1 reads and
bumps <Version> in Directory.Build.props (Get/Set-BuildPropsVersion)
instead of the csproj. DocsConsistencyTests.VersionMatchesDirectoryBuildProps
compares the props version with the built assembly version and asserts
the csproj carries no second <Version>. 505 passing.
(a) The NUGET_API_KEY check ran before packing and demanded the key even with -SkipPush; it now happens only right before the push, so pack-only runs work keyless. (b) The package was picked as 'newest *.nupkg by LastWriteTime' - any later touch of the artifacts folder could grab the wrong file. The exact $ProjectName.$version.nupkg is selected now (missing = hard error), and all push/cleanup references use that path. (c) NOT REPRODUCED: dotnet pack already checks $LASTEXITCODE, summaries the failure and stops via Write-Error before anything is deleted; the existing delete-artifact-on-failed-push behavior is unchanged. Verified live: two -SkipPush runs WITHOUT NUGET_API_KEY pack 1.0.0 and auto-bump-pack 1.0.1, both exiting cleanly with no key demand.
configuration.md: a Snapshot section paragraph for SensitiveFilePatterns/MaskSensitiveLines (what is dropped and redacted, attachment copy only, disk unchanged, marker line) and Other-files entries for session.json corruption tolerance and the best-effort turn log with output redaction. agentscript-spec.md: @@done ends the turn - later commands are skipped with 'after @@done'. README: the 0/1/2/130 exit-code table and a Building-from-source note on the single version source (Directory.Build.props) and publish.ps1 -SkipPush being keyless. CLI README: the exit-codes one-liner. DOCS-TODO.md deleted. Consistency tests (config keys/defaults, KA_ codes, CLI options/slash commands) stay green: 505 passing.
The last sequential fallback waits are gone: both capture strategies' Copy-control click loops (3s ClickAsync timeout per MessageCopyButton fallback) now use ClickNewestCopyControlAsync - ONE visible-filtered Or-wait over the whole fallback list carrying the same 3s-per-fallback total budget, then a single click on the matched element. A click failure is logged and reported as not-landed (Enter-safe), never a per-fallback retry. Test: with the first two MessageCopyButton fallbacks absent and the third visible, the capture clicks exactly the visible fallback after ONE combined wait per strategy - zero ClickAsync calls on the absent fallbacks. Stub lessons baked in: NSubstitute is last-config wins, the Or-chain must be closed on its own stub, and ILocator.EvaluateAsync is the 3-arg Task<JsonElement?> overload. 506 passing.
1. agentscript-spec error catalog: four duplicated rows removed (CONFLICTING_CONTROL, BAD_OPTION, COMMAND_NOT_FOUND, GIT_NOT_INSTALLED each kept once with the richer description). New ErrorCodes_AppearExactlyOnceInTheCatalogTest parses only the catalog rows and asserts every ErrorCodes constant appears exactly once. 2. @@next optional MSG: the parser's required/optional payload split now allows an optional <<<MSG on @@next (bare @@next and empty MSG stay valid; unknown sections still rejected with KA_PARSE_UNEXPECTED_ PAYLOAD). system-prompt.md and the per-message reminder show the optional MSG. Tests: NextWithMsg, BareNext, NextWithUnknownSection. 3. @@say after @@done now still reaches the user (executed after the done, not counted toward MaxCommandsPerResponse); every other remaining command is skipped with 'after @@done' as before. SayAfterDone_IsShown asserts both messages shown, x.txt uncreated, newfile skipped in the turn log. 4. IsSendEnabledAsync's inline JSON parse extracted into IsDisabledFromJson (disposed JsonDocument, false for null/empty/ invalid). PlaywrightChatPageTests covers true/false/null/not-json. 515 passing.
ImanEstiri
added a commit
that referenced
this pull request
Oct 31, 2026
…ed duplicates (a) User interactions (approvals, menus, @@ask) are timed as a separate 'user-wait' phase via a UserWaitTimingUi decorator that wraps the UI the loop and executors use - so approval/menu/ask time no longer inflates the execute or git-checks phases. (b) The environment probe now STARTS before the browser connection and runs concurrently with it, and is cached in .agent/env.json for 24h (keyed by the workspace; invalidated when older, missing, or with the new --refresh-env flag on run/interactive). The awaited result is read just before the first send, so the probe cost hides inside the connect phase. (c) FormatTable numbers duplicate phases (#1..#N) when a phase appears more than once; single occurrences keep their plain name. (d) TOTAL now shows 'TOTAL (wall clock)' and 'TOTAL without user-wait' (= wall clock minus accumulated user-wait). Tests: duplicate numbering, wall clock/without user-wait rows, user-wait subtraction; session timing test updated for send-confirm#1. 536 passing.
ImanEstiri
added a commit
that referenced
this pull request
Nov 5, 2026
main's PR #1 was a squash of an EARLIER snapshot of this same branch, so every overlapping file conflicted add/add; each resolves to the feature version (the newest state - main's README, for example, is byte-identical to the feature's pre-restructure copy). Everything main added that the feature never touched (.editorconfig, .gitattributes, .github workflow, global.json, nuget.config, LICENSE.txt, Directory.Packages.props, src/Playground) carries over unchanged. Merged tree: build clean, 641 core + 211 WPF tests green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Testing
Next Step
Start WPF development in a separate feature branch after merging.