native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352import Testing
@testable import Prowl
struct ClaudeScreenProfileTests { @Test func composerContentsPreservesDraftsAndAttachments() { for draft in ["", "hello", "hello\n second line", "[Image #1]", "[Pasted text #1 +20 lines]"] { let screen = AgentScreenSnapshot(text: "──────── Title ─\n❯ " + draft + "\n────────\nstatus") #expect(ClaudeScreenProfile.composerContents(in: screen) == draft) } #expect(ClaudeScreenProfile.composerContents(in: AgentScreenSnapshot(text: "❯ old transcript")) == nil) #expect(ClaudeScreenProfile.composerContents(in: AgentScreenSnapshot(text: "────\n❯ draft")) == nil) }
@Test func scrolledTranscriptRetainsTheLastLiveState() { let composer = "────────────────────\n❯ \n────────────────────\nstatus" let history = "⏺ Old response\n 10 Jump to bottom (click) ↓ \n\n" + composer let detection = ClaudeScreenProfile.detect(in: AgentScreenSnapshot(text: history)) #expect(detection.state == .unknown) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.viewer))
for live in ["✽ Thinking… (14s · still thinking)\n\n", ""] { var machine = AgentStateMachine() let initial = ClaudeScreenProfile.detect(in: AgentScreenSnapshot(text: live + composer)) _ = machine.receive(.screen(initial), now: 0) #expect(machine.receive(.screen(detection), now: 1).state == initial.state) #expect(machine.receive(.screen(detection), now: 300).state == initial.state) let idle = ClaudeScreenProfile.detect(in: AgentScreenSnapshot(text: composer)) #expect(machine.receive(.screen(idle), now: 301).state == .idle) } }
@Test func scrollOverlayCanCoverTheMiddleOfTranscriptText() { for label in ["Jump to bottom", "1 new message", "2 new messages"] { let text = """ ⏺ Earlier output Left transcript text \(label) (click) ↓ right transcript text
──────────────────── ❯ ──────────────────── status """ let detection = ClaudeScreenProfile.detect(in: AgentScreenSnapshot(text: text)) #expect(detection.state == .unknown) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.viewer)) } }
@Test func jumpToBottomTextOutsideTheScrollOverlayIsNotAViewer() { for label in ["Jump to bottom", "1 new message", "2 new messages"] { for text in [ "⏺ \(label) (click) ↓\n More response text\n\n────────────────────\n❯ \n────────────────────", "────────────────────\n❯ \(label) (click) ↓\n────────────────────", "────────────────────\n❯ \n────────────────────\n\(label) (click) ↓", "⏺ \(label) (click) ↓", ] { let detection = ClaudeScreenProfile.detect(in: AgentScreenSnapshot(text: text)) #expect(detection.state == .idle) #expect(detection.reason != .matched(ClaudeScreenProfile.RuleID.viewer)) } } }
@Test func ruleIDsAreUniqueAndRuntimePrefixed() { let ruleIDs = ClaudeScreenProfile.RuleID.all
#expect(Set(ruleIDs).count == ruleIDs.count) #expect(ruleIDs.allSatisfy { $0.rawValue.hasPrefix("claude.") }) }
@Test func capturedFixturesHaveStableReasonsIncludingViewerFix() throws { let expectedReasons: [String: AgentScreenDetectionReason] = [ "claude/2.1.223/blocked/command-permission.txt": .matched( ClaudeScreenProfile.RuleID.blockedPrompt ), "claude/2.1.223/blocked/workspace-trust.txt": .matched( ClaudeScreenProfile.RuleID.blockedPrompt ), "claude/2.1.223/idle/composer.txt": .matched(ClaudeScreenProfile.RuleID.idleComposer), "claude/2.1.223/idle/quoted-permission.txt": .matched( ClaudeScreenProfile.RuleID.idleComposer ), "claude/2.1.223/unknown/676-history-search-viewer.txt": .matched( ClaudeScreenProfile.RuleID.viewer ), "claude/2.1.223/working/backgrounded-subagent.txt": .matched( ClaudeScreenProfile.RuleID.spinner ), "claude/2.1.223/working/foreground-spinner.txt": .matched( ClaudeScreenProfile.RuleID.spinner ), "claude/2.1.223/working/subagent-active.txt": .matched( ClaudeScreenProfile.RuleID.spinner ), "claude/2.1.224/working/676-background-agent-wait.txt": .matched( ClaudeScreenProfile.RuleID.backgroundWork ), "claude/2.1.224/working/676-compound-elapsed-status.txt": .matched( ClaudeScreenProfile.RuleID.elapsedStatus ), "claude/2.1.226/working/676-wrapped-background-agent-wait.txt": .matched( ClaudeScreenProfile.RuleID.backgroundWork ), "claude/2.1.270/unknown/scrolled-transcript.txt": .matched(ClaudeScreenProfile.RuleID.viewer), "claude/2.1.266/idle/titled-composer.txt": .matched(ClaudeScreenProfile.RuleID.idleComposer), "claude/2.1.266/working/titled-border-spinner.txt": .matched( ClaudeScreenProfile.RuleID.spinner ), ] let fixtures = try AgentScreenFixtureCorpus.load().filter { $0.agent == .claude }
#expect(fixtures.count == expectedReasons.count) for fixture in fixtures { let detection = DetectedAgent.claude.detectScreen(in: fixture.text) #expect(detection.state == fixture.expectedState) #expect(detection.reason == expectedReasons[fixture.relativePath]) } }
@Test func titledComposerBorderStillFramesTheComposer() { let working = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ ✢ Crafting… (19s · still thinking with xhigh effort) ⎿ Tip: Use /permissions to pre-approve tools
──────────────────── Titled composer border probe ─ ❯ ─────────────────────────────────────────────────── """ ) ) #expect(working.state == .working) #expect(working.reason == .matched(ClaudeScreenProfile.RuleID.spinner))
let idle = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ ⏺ Done.
──────────────────── Titled composer border probe ─ ❯ ─────────────────────────────────────────────────── """ ) ) #expect(idle.state == .idle) #expect(idle.reason == .matched(ClaudeScreenProfile.RuleID.idleComposer))
// A chip without its closing rule character is prose, not a border. let unterminated = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ ✢ Crafting… (19s · still thinking with xhigh effort) ──────────────────── Titled composer border probe ❯ ─────────────────────────────────────────────────── """ ) ) #expect(unterminated.reason == .noRuleMatched) }
@Test func elapsedAndBackgroundWorkHaveDistinctReasons() { let elapsed = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ ● Forging… (10s · thinking with high effort) ───────── ❯ ───────── """ ) ) #expect(elapsed.state == .working) #expect(elapsed.reason == .matched(ClaudeScreenProfile.RuleID.elapsedStatus))
let background = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ Task complete. ───────── ❯ ───────── ◯ scout Map idle detection 3/5 agents done · 7m 29s """ ) ) #expect(background.state == .working) #expect(background.reason == .matched(ClaudeScreenProfile.RuleID.backgroundWork)) }
@Test func blockerOutranksRetainedSpinner() { let detection = ClaudeScreenProfile.detect( in: AgentScreenSnapshot( text: """ ✻ Tempering… (12s · esc to interrupt) Do you want to proceed? ❯ 1. Yes 2. No Esc to cancel · Tab to amend """ ) )
#expect(detection.state == .blocked) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.blockedPrompt)) }
@Test func blockerTextPreservesClaudeQuestionChoicesAndKeyboardHints() throws { let fixture = try AgentScreenFixtureCorpus.load() .first { $0.relativePath == "claude/2.1.223/blocked/command-permission.txt" } let text = try #require(fixture).text
let blocker = ClaudeScreenProfile.blockerText(in: AgentScreenSnapshot(text: text))
#expect(blocker?.contains("Do you want to proceed?") == true) #expect(blocker?.contains("❯ 1. Yes") == true) #expect(blocker?.contains("3. No") == true) #expect(blocker?.contains("Esc to cancel · Tab to amend · ctrl+e to explain") == true) }
@Test func spinnerAboveLongTodoListStaysWorking() { // Regression: a bottom-measured line budget (the shared recent-line tail) // let a long todo list plus the composer and a multi-line status line push // the live spinner row out of the detector window, so a working agent was // reported idle via the idle-composer rule. The todo block is sized from // the tail limit so this screen keeps overflowing it if the limit moves. var lines = [ "✻ Implementing hybridTopK… (5m 5s · ↓ 21.1k tokens)", " ⎿ \u{00A0}✔ Write failing tests for hybridTopK and per-scope pickCandidates", ] for index in 2...(agentDetectionRecentLineLimit + 2) { lines.append(" ◻ Todo item number \(index) still pending") } lines.append( contentsOf: [ "──────────────────────────────────────────", "❯ ", "──────────────────────────────────────────", " [Fable 5 | Enterprise] ██░░░░░░░░ 16% | will git:(master*)", " ███░░░░░░░ 34% (4h 3m / 5h) | ███░░░░░░░ 29% (4d 19h / 7d)", " ✓ Bash ×7 | ✓ Read ×6 | ✓ Edit ×5", " ▸ Implement hybridTopK in similarity.ts (2/16)", " ⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents", ] ) let nonBlankCount = lines.count { $0.contains { !$0.isWhitespace } } #expect(nonBlankCount > agentDetectionRecentLineLimit)
let detection = DetectedAgent.claude.detectScreen(in: lines.joined(separator: "\n"))
#expect(detection.state == .working) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.spinner)) }
@Test func spinnerAboveQueuedMessagesAndTipStaysWorking() { // The live status block is bounded by row shape, not by a fixed row // count: a "⎿" tip attachment, queued "❯" messages, and the right-aligned // effort chrome all belong to the block and must not displace the spinner. let text = """ ⏺ Running 1 shell command… ⎿ $ touch permission-probe.txt
✻ Slithering… (16s · ↓ 21.1k tokens) ⎿ Tip: Use ctrl+v to paste images from your clipboard
❯ Run the shell command `sleep 8`, then reply exactly DONE. ❯ Then summarize the diff in one line. ◉ xhigh · /effort ────────────────────────────────────────── ❯ Press up to edit queued messages ────────────────────────────────────────── """
let detection = DetectedAgent.claude.detectScreen(in: text)
#expect(detection.state == .working) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.spinner)) }
@Test func spinnerAboveQueuedMessageWithBlankLineStaysWorking() { // Live capture: a queued "❯" message whose body contains a blank line // renders its trailing paragraph as an unmarked prose row. The bottom-up // shape scan must not end the live block there — the paragraph still // belongs to the queued block, and stopping demoted the spinner above it // to history, so a working agent read as idle. let text = """ ⏺ Waiting for a new run to appear on the poc repo · 2m 35s ⎿ $ until gh api "repos/acme/poc/actions/runs" >/dev/null; do sleep 5; done (2m 35s) (ctrl+b to run in background)
✻ Combobulating… (14m 4s · ↓ 141.9k tokens)
❯ https://example.com/acme/poc/actions/runs/18352009
这个是你发的么 ❯ https://example.com/acme/poc/actions/runs/18352094 还有这个
────────────────────────────────────────── ❯ Press up to edit queued messages ────────────────────────────────────────── ✓ Bash ×18 | ✓ Edit ×1 ⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents """
let detection = DetectedAgent.claude.detectScreen(in: text)
#expect(detection.state == .working) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.spinner)) }
@Test func spinnerQuotedAboveTranscriptBlockStaysIdle() { // A status row quoted in the transcript sits above a "⏺" block head. The // live block scan stops at that head, so reading the full active screen // must not resurrect the quoted spinner as live activity. var lines = [ "✻ Tempering… (12s · esc to interrupt)", "", "⏺ Here is the analysis:", ] lines.append(contentsOf: (1...20).map { " analysis detail line \($0)" }) lines.append( contentsOf: [ "", "✻ Crunched for 7s", "", "──────────────────────────────────────────", "❯ ", "──────────────────────────────────────────", " ⏸ manual mode on · ← for agents", ] )
let detection = DetectedAgent.claude.detectScreen(in: lines.joined(separator: "\n"))
#expect(detection.state == .idle) #expect(detection.reason == .matched(ClaudeScreenProfile.RuleID.idleComposer)) }
@Test func unstructuredScreenUsesExplicitFallback() { let detection = ClaudeScreenProfile.detect( in: AgentScreenSnapshot(text: "screen without live Claude chrome") )
#expect(detection.state == .idle) #expect(detection.reason == .noRuleMatched) }}