-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Add sidebar active agent indicator #6480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
75 commits
Select commit
Hold shift + click to select a range
71ea935
Add sidebar active agent indicator
lawrencecchen 3bea463
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen 7ac7b72
Sidebar spinner: GPU-pure macOS spokes + debug spinner gallery
lawrencecchen 13080a1
Fix vendor/bonsplit pointer dropped by merge
lawrencecchen edc0e29
Wire sidebar.showAgentActivity through config pipeline + localize set…
lawrencecchen 0896f4f
Honor Hide All Sidebar Details for the agent activity indicator
lawrencecchen 4bed4e7
Add default test stub for controlDebugShowSpinnerGallery
lawrencecchen d5401f7
Document debug-only verbatim localization exception for spinner gallery
lawrencecchen 486f1b8
Import QuartzCore in sidebar spinner for explicit Core Animation deps
lawrencecchen 8a210bd
Scope sidebar agent observation to the running-agent count
lawrencecchen 9ca19b6
Split reusable GPUSpinner into its own file + document showAgentActiv…
lawrencecchen 40136d3
Sidebar spinner: grey in both states + thinner spokes
lawrencecchen 750a90a
Spinner gallery: add overlay comparison (native vs GPU spokes) for fr…
lawrencecchen 652b94d
Spinner gallery: float window + makeKey so frame-capture targets it
lawrencecchen 59f3a4b
Spinner gallery: size-match overlay to native intrinsic spinner size
lawrencecchen 5b09c4b
GPU spinner: match native macOS spoke proportions (compact ring, cent…
lawrencecchen 629a11b
Add 'cmux loading <on|off>' CLI command for the workspace loading ind…
lawrencecchen e36af2f
Move loading command under 'cmux workspace loading'; spinner-only on …
lawrencecchen 87243c5
Sidebar: move loading spinner into the far-right close-button slot
lawrencecchen 332b12d
Keep loading spinner visible under Hide All Sidebar Details
lawrencecchen 2a0e0dc
Phase-lock all GPU spinners to a shared media-clock grid
lawrencecchen 037a3eb
Sidebar loading spinner: left/right position setting + fade/cross-fad…
lawrencecchen 2bdec80
Spinner: remove per-row animations (LazyVStack height jank) + default…
lawrencecchen 63500a9
Re-add height-neutral title push animation for the leading status slot
lawrencecchen 496b559
Add notification badge position setting + opacity fades for badge & s…
lawrencecchen 18b192f
Workspace-scope cmux workspace loading + report before/after state
lawrencecchen a435c8f
Restore the title x-slide: animate leading slot width, not just opacity
lawrencecchen 5e18577
Use an interruptible spring for the status slide/fade (Apple-style)
lawrencecchen 7052a2d
Self-review cleanups: docs, validation, appearance, tests
lawrencecchen 1ee34fb
Fix autoreview findings: hibernation ignores manual loaders; validate…
lawrencecchen 1c6a4e2
Wire the two position rows into the Settings search index
lawrencecchen 950b719
Fix autoreview findings: hibernation activity + localized workspace help
lawrencecchen f22821c
Localize the workspace loading CLI usage/error strings (en/ja/ko/uk)
lawrencecchen d64ddc9
Policy cleanups: dedicated file + DocC for new public package symbols
lawrencecchen be803a2
Trim comment noise; build spinner tooltip only in gated branches
lawrencecchen ddd8013
Note the intentional badge/spinner slot-sharing on the leading side
lawrencecchen 93650fb
Right-positioned badge sticks flush to the row's right corner
lawrencecchen 18e7f43
Merge origin/main into feat-agent-activity-indicator
lawrencecchen 37952eb
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen f8ae21d
Address PR bot findings: spinner immediacy under context menu + toolt…
lawrencecchen 6e45be7
Count running agents without intermediate filter allocation
lawrencecchen be916ad
Fix CI + review findings: Cmd+W registration, schema aliases, full lo…
lawrencecchen 32658f8
workspace_loading on: clear the manual key from all panels before set…
lawrencecchen 3554f26
Retrigger CI (no workflow runs were created for the previous push)
lawrencecchen 652428b
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen 48d4070
Refresh Swift file length budget for the loading-indicator feature gr…
lawrencecchen 9c7f7db
Retrigger stuck Vercel deployments
lawrencecchen 7cb9cb2
Assert the deferred-invalidation flag in the context-menu spinner test
lawrencecchen 1ed87a6
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen 3f45679
Bound manual loader ids and count on the socket path
lawrencecchen 71616b4
set_agent_lifecycle rejects manual keys; workspace_loading owns the n…
lawrencecchen 66afc78
Manual loaders survive panel lifecycle; reserve the manual namespace
lawrencecchen de8c56a
Refresh Swift file length budget (+24 lines, review fixes)
lawrencecchen 46c7ef6
Fix inactive sidebar row alignment
lawrencecchen 30b53d5
Fix workspace loading off state reporting
lawrencecchen e171a8d
Tighten sidebar spinner title spacing
lawrencecchen 843734d
Resolve workspace loading refs before v1 bridge
lawrencecchen e6cd9a7
Address agent activity merge policy findings
lawrencecchen b3cdfc8
Split spinner gallery energy type
lawrencecchen 979658e
Fix workspace loading failure reporting
lawrencecchen 6ce6fdf
Split sidebar v1 test fake
lawrencecchen 245a129
Reject malformed workspace loading flags
lawrencecchen e23775e
Gate hidden agent activity row churn
lawrencecchen 8cd96c1
Reject blank workspace loading tab targets
lawrencecchen 02c5784
Localize agent activity tooltips
lawrencecchen 944a48e
Move workspace loading parser into CLI file
lawrencecchen f466c31
Merge origin/main into feat-agent-activity-indicator
lawrencecchen fe5e88b
Split agent activity indicator code
lawrencecchen aded6c6
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen c87b34b
Register spinner gallery auxiliary window
lawrencecchen ae6500e
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen d98b582
Keep sidebar snapshot policy tests below budget
lawrencecchen 93903e8
Shorten CLI socket test home path
lawrencecchen 28cc691
Merge remote-tracking branch 'origin/main' into feat-agent-activity-i…
lawrencecchen 3036399
Merge origin/main into agent activity indicator
lawrencecchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import Foundation | ||
|
|
||
| struct WorkspaceLoadingArguments { | ||
| let turnOn: Bool | ||
| let id: String? | ||
| let workspace: String? | ||
| let window: String? | ||
| } | ||
|
|
||
| extension CMUXCLI { | ||
| func validateWorkspaceLoadingCommandBeforeSocket( | ||
| command: String, | ||
| commandArgs: [String] | ||
| ) throws { | ||
| guard command == "workspace", | ||
| commandArgs.first?.lowercased() == "loading" else { | ||
| return | ||
| } | ||
| _ = try parseWorkspaceLoadingArguments(Array(commandArgs.dropFirst())) | ||
| } | ||
|
|
||
| func workspaceLoadingUsage() -> String { | ||
| String( | ||
| localized: "cli.workspaceLoading.usage", | ||
| defaultValue: "Usage: cmux workspace loading <on|off> [--id <name>] [--workspace <id>] [--window <id>] [--json]" | ||
| ) | ||
| } | ||
|
|
||
| func parseWorkspaceLoadingArguments(_ commandArgs: [String]) throws -> WorkspaceLoadingArguments { | ||
| let usage = workspaceLoadingUsage() | ||
| var idArg: String? | ||
| var wsArg: String? | ||
| var winArg: String? | ||
| var positional: [String] = [] | ||
| var index = 0 | ||
| var pastTerminator = false | ||
|
|
||
| func requireValue() throws -> String { | ||
| let valueIndex = index + 1 | ||
| guard valueIndex < commandArgs.count, !commandArgs[valueIndex].hasPrefix("--") else { | ||
| throw CLIError(message: usage) | ||
| } | ||
| return commandArgs[valueIndex] | ||
| } | ||
|
|
||
| while index < commandArgs.count { | ||
| let arg = commandArgs[index] | ||
| if !pastTerminator, arg == "--" { | ||
| pastTerminator = true | ||
| index += 1 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg == "--json" { | ||
| index += 1 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg == "--id" { | ||
| idArg = try requireValue() | ||
| index += 2 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg == "--workspace" { | ||
| wsArg = try requireValue() | ||
| index += 2 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg == "--window" { | ||
| winArg = try requireValue() | ||
| index += 2 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg.hasPrefix("--id=") { | ||
| let value = String(arg.dropFirst("--id=".count)) | ||
| guard !value.isEmpty else { throw CLIError(message: usage) } | ||
| idArg = value | ||
| index += 1 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg.hasPrefix("--workspace=") { | ||
| let value = String(arg.dropFirst("--workspace=".count)) | ||
| guard !value.isEmpty else { throw CLIError(message: usage) } | ||
| wsArg = value | ||
| index += 1 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg.hasPrefix("--window=") { | ||
| let value = String(arg.dropFirst("--window=".count)) | ||
| guard !value.isEmpty else { throw CLIError(message: usage) } | ||
| winArg = value | ||
| index += 1 | ||
| continue | ||
| } | ||
| if !pastTerminator, arg.hasPrefix("--") { | ||
| throw CLIError(message: usage) | ||
| } | ||
| positional.append(arg) | ||
| index += 1 | ||
| } | ||
|
|
||
| guard positional.count <= 1 else { | ||
| throw CLIError(message: usage) | ||
| } | ||
| guard let sub = positional.first?.lowercased() else { | ||
| throw CLIError(message: usage) | ||
| } | ||
| let turnOn: Bool | ||
| switch sub { | ||
| case "on", "start", "show", "running", "busy": | ||
| turnOn = true | ||
| case "off", "stop", "hide", "done", "idle", "finished": | ||
| turnOn = false | ||
| default: | ||
| throw CLIError(message: String( | ||
| format: String( | ||
| localized: "cli.error.workspaceLoadingInvalidState", | ||
| defaultValue: "Invalid state '%@'. Expected on or off. %@" | ||
| ), | ||
| locale: .current, | ||
| sub, | ||
| usage | ||
| )) | ||
| } | ||
|
|
||
| return WorkspaceLoadingArguments( | ||
| turnOn: turnOn, | ||
| id: idArg, | ||
| workspace: wsArg, | ||
| window: winArg | ||
| ) | ||
| } | ||
|
|
||
| /// `cmux workspace loading <on|off> [--id <name>]` toggles the workspace's | ||
| /// loading spinner via the reserved `manual` lifecycle namespace. | ||
| func runWorkspaceLoading( | ||
| commandArgs: [String], | ||
| client: SocketClient, | ||
| windowId: String?, | ||
| jsonOutput: Bool | ||
| ) throws { | ||
| let parsed = try parseWorkspaceLoadingArguments(commandArgs) | ||
| let usage = workspaceLoadingUsage() | ||
|
|
||
| let manual = AgentHibernationLifecycleStatusKeys.manualKey | ||
| let key: String | ||
| if let rawId = parsed.id?.trimmingCharacters(in: .whitespacesAndNewlines) { | ||
| guard !rawId.isEmpty else { | ||
| throw CLIError(message: usage) | ||
| } | ||
| let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") | ||
| guard rawId.unicodeScalars.allSatisfy(allowed.contains) else { | ||
| throw CLIError(message: String( | ||
| format: String( | ||
| localized: "cli.error.workspaceLoadingInvalidId", | ||
| defaultValue: "Invalid --id '%@'. Use letters, digits, '.', '_', or '-' (no spaces)." | ||
| ), | ||
| locale: .current, | ||
| rawId | ||
| )) | ||
| } | ||
| key = "\(manual):\(rawId)" | ||
| } else { | ||
| key = manual | ||
| } | ||
|
|
||
| let windowRaw = parsed.window ?? windowId | ||
| let workspaceArg = parsed.workspace ?? Self.callerWorkspaceForSurfaceHandle(nil, windowRaw: windowRaw) | ||
| let winId = try normalizeWindowHandle(windowRaw, client: client) | ||
| let wsId = try resolveWorkspaceId( | ||
| workspaceArg, | ||
| client: client, | ||
| windowHandle: winId | ||
| ) | ||
|
|
||
| let response = try sendV1Command( | ||
| "workspace_loading \(key) \(parsed.turnOn ? "on" : "off") --tab=\(wsId)", | ||
| client: client | ||
| ) | ||
|
|
||
| if jsonOutput { | ||
| var before = false | ||
| var after = false | ||
| for part in response.split(separator: ";") { | ||
| let kv = part.split(separator: "=", maxSplits: 1) | ||
| guard kv.count == 2 else { continue } | ||
| let isOn = kv[1].trimmingCharacters(in: .whitespaces).uppercased() == "ON" | ||
| if kv[0] == "before" { before = isOn } | ||
| if kv[0] == "after" { after = isOn } | ||
| } | ||
| print(jsonString([ | ||
| "ok": true, | ||
| "id": parsed.id ?? "", | ||
| "workspace_id": wsId, | ||
| "before": before, | ||
| "after": after, | ||
| "loading": after, | ||
| ])) | ||
| } else { | ||
| print(response) | ||
| } | ||
| } | ||
| } | ||
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
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
52 changes: 52 additions & 0 deletions
52
...ntrolSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarWorkspaceLoadingV1.swift
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| internal import Foundation | ||
|
|
||
| extension ControlCommandCoordinator { | ||
| /// `workspace_loading <key> <on|off> [--tab=<id>]` toggles a workspace-scoped | ||
| /// manual loading state and replies `before=ON;after=OFF`. | ||
| nonisolated func sidebarWorkspaceLoading(_ args: String, context: (any ControlCommandContext)?) -> String { | ||
| let parsed = sidebarParseOptions(args) | ||
| let usage = "workspace_loading <key> <on|off> [--tab=<id>]" | ||
| guard parsed.positional.count >= 2 else { | ||
| return "ERROR: Usage: \(usage)" | ||
| } | ||
| let key = parsed.positional[0] | ||
| guard key == "manual" || key.hasPrefix("manual:") else { | ||
| return "ERROR: workspace_loading only accepts manual loader keys (manual or manual:<id>); use set_agent_lifecycle for agent keys" | ||
| } | ||
| if key != "manual" { | ||
| let id = key.dropFirst("manual:".count) | ||
| let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") | ||
| guard !id.isEmpty, id.count <= 64, id.unicodeScalars.allSatisfy(allowed.contains) else { | ||
| return "ERROR: Invalid manual loader id; use 1-64 characters from letters, digits, '.', '_', '-'" | ||
| } | ||
| } | ||
| let on: Bool | ||
| switch parsed.positional[1].lowercased() { | ||
| case "on", "running", "start", "show": | ||
| on = true | ||
| case "off", "idle", "stop", "hide": | ||
| on = false | ||
| default: | ||
| return "ERROR: Usage: \(usage)" | ||
| } | ||
| let tabArg = parsed.options["tab"] | ||
| if let tabArg, tabArg.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { | ||
| return "ERROR: Invalid --tab; expected a workspace id, ref, or index" | ||
| } | ||
| let result = context?.controlSidebarOnMain { | ||
| $0.controlSidebarSetWorkspaceLoading( | ||
| tabArg: tabArg, | ||
| key: key, | ||
| on: on | ||
| ) | ||
| } | ||
| guard let result = result ?? nil else { | ||
| return "ERROR: Workspace not found" | ||
| } | ||
| if let failureReason = result.failureReason { | ||
| return "ERROR: \(failureReason)" | ||
| } | ||
| func label(_ value: Bool) -> String { value ? "ON" : "OFF" } | ||
| return "before=\(label(result.before));after=\(label(result.after))" | ||
| } | ||
| } |
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
25 changes: 25 additions & 0 deletions
25
...t/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarWorkspaceLoadingState.swift
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /// The manual loader key state before and after a `workspace_loading` toggle, | ||
| /// reported back to the caller (e.g. `before=ON;after=OFF`). | ||
| public struct ControlSidebarWorkspaceLoadingState: Sendable, Equatable { | ||
| /// Whether this manual loader key was running before the toggle. | ||
| public let before: Bool | ||
|
|
||
| /// Whether this manual loader key is running after the toggle. | ||
| public let after: Bool | ||
|
|
||
| /// A human-readable failure reason when the requested toggle could not be | ||
| /// applied. The v1 line protocol maps this to `ERROR: <reason>`. | ||
| public let failureReason: String? | ||
|
|
||
| /// Creates a before/after pair for one `workspace_loading` toggle. | ||
| /// | ||
| /// - Parameters: | ||
| /// - before: Spinner state before the change. | ||
| /// - after: Spinner state after the change. | ||
| /// - failureReason: A failure reason when no state change was possible. | ||
| public init(before: Bool, after: Bool, failureReason: String? = nil) { | ||
| self.before = before | ||
| self.after = after | ||
| self.failureReason = failureReason | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.