Skip to content
Merged
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
4853e69
fix(executors): truthful executor rows, startup reconciliation, resum…
point-source Jul 6, 2026
b83115d
test(websocket): stabilize origin connection cleanup
point-source Jul 15, 2026
8e38117
feat: accept input while a session's foreground turn waits on backgro…
point-source Jul 11, 2026
dba66b1
fix(orchestrator): address automated review of the busy-signal PR
point-source Jul 11, 2026
77890a1
fix(orchestrator): close prompt race and pin the Monitor view contract
point-source Jul 13, 2026
18c6468
fix(orchestrator): attest Monitor provenance and make the foreground …
point-source Jul 13, 2026
80e3500
fix(orchestrator): serialize foreground prompt admission
carlosflorencio Jul 13, 2026
4ab3443
fix(orchestrator): enforce prompt dispatch callback contract
carlosflorencio Jul 13, 2026
6a88c5c
test(orchestrator): align prompt dispatch callback mock
carlosflorencio Jul 13, 2026
77c23e7
fix(orchestrator): reconcile busy signal with prompt generations
carlosflorencio Jul 14, 2026
6756763
docs: renumber busy-signal ADR references
carlosflorencio Jul 14, 2026
c8e427d
docs(spec): fine-grained background-running status indicator
point-source Jul 18, 2026
f9a9df8
refactor(web): solidify tri-state session icon contract as single source
point-source Jul 18, 2026
cf7826a
fix(web): surface background-running substate in session-reopen menu
point-source Jul 18, 2026
0760ce3
feat(web): show background-running substate in sidebar task indicator
point-source Jul 18, 2026
a5051e6
docs(spec): mark session-level-indicator complete, vocabulary in prog…
point-source Jul 18, 2026
82e0d4e
feat(task): add task-level foreground-activity aggregate to TaskDTO
point-source Jul 18, 2026
24e509f
feat(task): compute and emit task-level activity aggregate live
point-source Jul 18, 2026
659f2ab
feat(web): extend getTaskStateIcon with task-level background affordance
point-source Jul 18, 2026
952c021
feat(web): show task-level background-running affordance on the kanba…
point-source Jul 18, 2026
0ab757f
feat(web): show task-level background-running affordance on task list…
point-source Jul 18, 2026
a5f4f7d
docs(spec): mark task-level-indicator in progress (backend + 2 surfaces)
point-source Jul 18, 2026
9536d0a
fix(web): show task-level background affordance when primary session …
point-source Jul 18, 2026
d9c6fea
feat(web): show task-level background affordance on graph/swimlane nodes
point-source Jul 18, 2026
4ae6206
feat(web): reflect task-level background affordance in open-task header
point-source Jul 18, 2026
d27cc0b
docs(spec): mark task-level-indicator and three-state-vocabulary comp…
point-source Jul 18, 2026
371b311
test(orchestrator): lock unknown-substate safe fallback through task …
point-source Jul 18, 2026
174000b
test(web): lock task-level safe fallback for a missing aggregate
point-source Jul 18, 2026
b146542
test(web): lock task.updated live-flip, done-clear, and clobber-guard…
point-source Jul 18, 2026
5786356
test(orchestrator): lock fresh-load wire correctness for the busy sub…
point-source Jul 18, 2026
60aa7dc
docs(spec): mark live-propagation-fallback complete
point-source Jul 18, 2026
0ffcd90
docs(task): correct busy-signal ADR reference to ADR-0043
point-source Jul 18, 2026
21b6e6b
fix(task): preserve last-known activity aggregate when the session se…
point-source Jul 18, 2026
6df95b1
feat(web): give task-level background-running a distinct hue on compa…
point-source Jul 18, 2026
c5de28c
fix: reconcile busy-signal rebase conflicts
point-source Jul 18, 2026
5cc366c
fix: retain storage activity during callback prompts
point-source Jul 18, 2026
ce02c98
docs(requirements): busy indicators reflect all in-flight work
point-source Jul 19, 2026
36db2eb
docs(spec): surface all in-flight work in task busy indicators
point-source Jul 19, 2026
b5f0761
feat(web): surface session substate in mobile sessions section
point-source Jul 19, 2026
621e8de
docs(spec): mark session-level-truth complete for mobile sessions
point-source Jul 19, 2026
8d4f474
feat(web): add in-flight predicate and task hook for destructive-acti…
point-source Jul 19, 2026
b604463
feat(web): warn before archiving a task that is still working
point-source Jul 19, 2026
ad3915f
feat(web): warn before deleting a task that is still working
point-source Jul 19, 2026
6ec87c2
docs(spec): mark destructive-action-guard complete
point-source Jul 19, 2026
d82d55d
fix(sidebar): read task-level foreground_activity aggregate for row s…
point-source Jul 19, 2026
499a9da
docs(spec): mark task-level-truth complete after sidebar aggregate al…
point-source Jul 19, 2026
9a0f555
fix(sidebar): carry foreground_activity aggregate onto the mobile tas…
point-source Jul 19, 2026
daa611d
feat(web): lift waiting-for-input variants into shared state vocabulary
point-source Jul 19, 2026
892fe45
feat(web): carry waiting-for-input parity to the kanban card
point-source Jul 19, 2026
641b9e6
feat(web): re-enable waiting-for-input reading on the task-list row
point-source Jul 19, 2026
cb3aeac
feat(web): carry waiting-for-input parity to graph and swimlane nodes
point-source Jul 19, 2026
8b599e0
feat(web): carry waiting-for-input parity to the open-task header
point-source Jul 19, 2026
a703344
feat(web): carry waiting-for-input parity to the session menus
point-source Jul 19, 2026
a86e00f
refactor(web): drop orphaned useTaskPendingClarification wrapper
point-source Jul 19, 2026
dfab3b2
docs(spec): mark waiting-for-input-parity and state-vocabulary complete
point-source Jul 19, 2026
93c7fae
refactor(web): collapse identical task/session permission icon constants
point-source Jul 19, 2026
d115a59
fix(web): close busy-indicator review gaps
point-source Jul 19, 2026
c1ba459
perf(web): skip hidden task activity lookups
point-source Jul 20, 2026
3f3df54
fix(web): preserve terminal session status icons
point-source Jul 20, 2026
3624dcb
fix(orchestrator): preserve claimed background-idle prompts
point-source Jul 20, 2026
9c39634
fix: preserve background liveness after foreground turns
point-source Jul 22, 2026
b5174b5
fix: prioritize pending input over task activity
point-source Jul 22, 2026
e67fb25
fix: allow prompts while background work continues
point-source Jul 22, 2026
d52096f
docs: complete background liveness follow-up plan
point-source Jul 22, 2026
1138eef
fix: reconcile async subagent lifecycle activity
point-source Jul 22, 2026
be0fafb
fix: address fine-grained busy signal review feedback
point-source Jul 23, 2026
2bb1581
test(web): align message handler routing tests
point-source Jul 23, 2026
67b36c8
test: align rebased integration fixtures
point-source Jul 23, 2026
bf527f3
fix: complete busy signal review remediation
point-source Jul 24, 2026
83047e2
docs: normalize background liveness governance
point-source Jul 24, 2026
b892e04
refactor(web): extract sidebar item mapping
point-source Jul 24, 2026
d08cf67
fix(orchestrator): ignore incomplete child tool updates
point-source Jul 25, 2026
a0040dd
fix(orchestrator): preserve tool activity ownership
point-source Jul 25, 2026
c935b1c
test(websocket): resolve rebased helper collision
point-source Jul 25, 2026
48b365d
fix(orchestrator): preserve prompt claim across resume
point-source Jul 25, 2026
7a211cd
fix(backend): address busy-signal review findings
point-source Jul 25, 2026
a213334
fix(web): address busy-signal review findings
point-source Jul 25, 2026
8ffc998
docs: cross-link busy-signal artifacts and document live-work warnings
point-source Jul 25, 2026
69552bf
fix: distinguish background work in chat status and clarify activity …
point-source Jul 25, 2026
464be03
fix(backend): retire uncorrelated background work FIFO and tombstone …
point-source Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions apps/backend/cmd/mock-agent/background_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"testing"
"time"

acp "github.com/coder/acp-go-sdk"
)

// TestParseBackgroundDuration pins the /background argument parsing, including
// the regression Copilot flagged on PR #3: a unit-bearing value like "1m" must
// not be mangled into "1ms" by the bare-seconds fallback.
func TestParseBackgroundDuration(t *testing.T) {
const def = 8 * time.Second
cases := []struct {
name string
cmd string
want time.Duration
}{
{"no argument uses default", "/background", def},
{"bare number is seconds", "/background 12", 12 * time.Second},
{"explicit seconds", "/background 20s", 20 * time.Second},
{"explicit minutes (regression: not 1ms)", "/background 1m", time.Minute},
{"explicit hours", "/background 2h", 2 * time.Hour},
{"explicit milliseconds", "/background 500ms", 500 * time.Millisecond},
{"unparseable falls back to default", "/background soon", def},
{"zero falls back to default", "/background 0", def},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := parseBackgroundDuration(tc.cmd, def); got != tc.want {
t.Fatalf("parseBackgroundDuration(%q) = %v, want %v", tc.cmd, got, tc.want)
}
})
}
}

func TestDetachedBackgroundLifecycleFrames(t *testing.T) {
e, updates := newTestEmitter()
e.launchAsyncSubagentTool("detached-1", "Detached work", "Keep working", "general-purpose")
e.completeDetachedWork()

got := updates.getUpdates()
if len(got) != 3 {
t.Fatalf("updates = %d, want launch, async acknowledgement, and completion", len(got))
}
if got[0].notification.Update.ToolCall == nil {
t.Fatal("first update must register the subagent tool")
}
launch := got[1].notification.Update.ToolCallUpdate
if launch == nil {
t.Fatal("second update must acknowledge the async launch")
}
response := launch.Meta["claudeCode"].(map[string]any)["toolResponse"].(map[string]any)
if response[subagentKeyStatus] != subagentStatusAsync || response["isAsync"] != true {
t.Fatalf("async launch response = %#v", response)
}

usage := got[2].notification.Update.UsageUpdate
if usage == nil {
t.Fatal("third update must be a usage lifecycle boundary")
}
origin := usage.Meta[claudeOriginMetaKey].(map[string]any)
if origin["kind"] != claudeOriginTaskNotification {
t.Fatalf("completion origin = %#v", origin)
}
if got[2].notification.SessionId != acp.SessionId("test-session") {
t.Fatalf("completion session = %q", got[2].notification.SessionId)
}
}

func TestAsyncSubagentForegroundFramesMatchClaudeOrdering(t *testing.T) {
e, updates := newTestEmitter()
emitAsyncSubagentLifecycle(e, "/async-subagent-teardown", false)

got := updates.getUpdates()
if len(got) != 5 {
t.Fatalf("updates = %d, want launch, async acknowledgement, idle, thought, and message", len(got))
}
if got[0].notification.Update.ToolCall == nil {
t.Fatal("first update must start the Agent tool")
}
launch := got[1].notification.Update.ToolCallUpdate
if launch == nil {
t.Fatal("second update must acknowledge the async launch")
}
response := launch.Meta["claudeCode"].(map[string]any)["toolResponse"].(map[string]any)
if response["agentId"] != asyncSubagentAgentID || response[subagentKeyStatus] != subagentStatusAsync {
t.Fatalf("async launch response = %#v", response)
}
idle := got[2].notification.Update.UsageUpdate
if idle == nil {
t.Fatal("third update must be the foreground-idle usage boundary")
}
origin := idle.Meta[claudeOriginMetaKey].(map[string]any)
if origin["kind"] != claudeOriginHuman {
t.Fatalf("idle origin = %#v", origin)
}
if !isThoughtUpdate(got[3]) {
t.Fatal("fourth update must be final same-prompt thinking")
}
if !isTextUpdate(got[4]) || getTextContent(got[4]) != "Foreground response after async launch." {
t.Fatalf("fifth update must be final same-prompt assistant output, got %#v", got[4])
}
}
70 changes: 70 additions & 0 deletions apps/backend/cmd/mock-agent/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,33 @@ func (e *emitter) endMonitorTool(id acp.ToolCallId) {
const (
subagentKeyStatus = "status"
subagentStatusCompleted = "completed"
subagentStatusAsync = "async_launched"
subagentKeyDescription = "description"
subagentKeyPrompt = "prompt"
subagentKeySubagentType = "subagent_type"
)

const (
claudeOriginMetaKey = "_claude/origin"
claudeOriginHuman = "human"
claudeOriginTaskNotification = "task-notification"
)

// foregroundIdle emits the human-origin usage boundary Claude sends after it
// hands the foreground back while detached work remains active.
func (e *emitter) foregroundIdle() {
_ = e.conn.SessionUpdate(e.ctx, acp.SessionNotification{
SessionId: e.sid,
Update: acp.SessionUpdate{UsageUpdate: &acp.SessionUsageUpdate{
Size: 1_000_000,
Used: 24_000,
Meta: map[string]any{
claudeOriginMetaKey: map[string]any{"kind": claudeOriginHuman},
},
}},
})
}

// subagentClaudeMeta builds the `_meta.claudeCode.toolName=Agent` payload that
// claude-agent-acp tags subagent (Task) tool_call notifications with. The
// kandev ACP adapter recognizes subagents by this marker.
Expand Down Expand Up @@ -242,6 +264,54 @@ func (e *emitter) startSubagentTool(id acp.ToolCallId, description, prompt, suba
})
}

// launchAsyncSubagentTool mirrors Claude's detached Agent launch: the Task
// invocation is terminal, but its independently-running workload is not.
func (e *emitter) launchAsyncSubagentTool(
id acp.ToolCallId,
description, prompt, subagentType string,
) {
e.startSubagentTool(id, description, prompt, subagentType)
withUpdateMeta := func(meta any) acp.ToolCallUpdateOpt {
return func(tu *acp.SessionToolCallUpdate) { tu.Meta = toMetaMap(meta) }
}
response := map[string]any{
"claudeCode": map[string]any{
"toolResponse": map[string]any{
"agentId": asyncSubagentAgentID,
"agentType": subagentType,
subagentKeyStatus: subagentStatusAsync,
"isAsync": true,
"outputFile": "/tmp/kandev-e2e-detached.output",
"canReadOutputFile": true,
},
},
}
_ = e.conn.SessionUpdate(e.ctx, acp.SessionNotification{
SessionId: e.sid,
Update: acp.UpdateToolCall(id,
acp.WithUpdateRawOutput("Async agent launched successfully."),
withUpdateMeta(response),
),
})
}

// completeDetachedWork emits the same task-notification usage boundary Claude
// sends when an async workload finishes. The provider does not expose the task
// ID on this frame, so the orchestrator retires it only when one registration is
// outstanding and the completion is therefore unambiguous.
func (e *emitter) completeDetachedWork() {
_ = e.conn.SessionUpdate(e.ctx, acp.SessionNotification{
SessionId: e.sid,
Update: acp.SessionUpdate{UsageUpdate: &acp.SessionUsageUpdate{
Size: 1_000_000,
Used: 25_000,
Meta: map[string]any{
claudeOriginMetaKey: map[string]any{"kind": claudeOriginTaskNotification},
},
}},
})
}

// completeSubagentTool emits the terminal subagent tool_call_update with the
// result text and the `toolResponse` metadata block.
func (e *emitter) completeSubagentTool(id acp.ToolCallId, resultText string, r subagentResult) {
Expand Down
119 changes: 119 additions & 0 deletions apps/backend/cmd/mock-agent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,77 @@ func handlePrompt(e *emitter, prompt, model string) {
emitMarkdownShowcase(e, model)
case strings.EqualFold(cmd, "/sleep") || strings.HasPrefix(strings.ToLower(cmd), "/sleep "):
emitSleep(e, cmd)
case strings.EqualFold(cmd, "/background") || strings.HasPrefix(strings.ToLower(cmd), "/background "):
emitBackgroundWork(e, cmd)
case strings.EqualFold(cmd, "/detached-background") || strings.HasPrefix(strings.ToLower(cmd), "/detached-background "):
emitDetachedBackgroundWork(e, cmd)
case strings.EqualFold(cmd, "/async-subagent-lifecycle") || strings.HasPrefix(strings.ToLower(cmd), "/async-subagent-lifecycle "):
emitAsyncSubagentLifecycle(e, cmd, true)
case strings.EqualFold(cmd, "/async-subagent-teardown"):
emitAsyncSubagentLifecycle(e, cmd, false)
default:
emitRandomResponse(e, cmd, model)
}
}

const asyncSubagentAgentID = "agent_e2e_async_lifecycle"

// emitAsyncSubagentLifecycle replays the ordering emitted by a detached Claude
// Agent rather than the shell-oriented /detached-background sequence:
//
// 1. Agent tool launch acknowledgement with a stable agentId
// 2. human-origin usage boundary (foreground idle)
// 3. final thought and assistant output from the same prompt
// 4. Prompt returns to its caller, completing the foreground turn
// 5. optional ID-less task-notification boundary after the child finishes
//
// The teardown variant intentionally omits step 5 so execution termination is
// the only evidence available to reconcile the registration.
func emitAsyncSubagentLifecycle(e *emitter, cmd string, emitCompletion bool) {
d := parseBackgroundDuration(cmd, 20*time.Second)
toolCallID := nextToolID()
e.launchAsyncSubagentTool(
toolCallID,
"Async subagent exploration",
"Continue independently after the foreground turn ends",
"general-purpose",
)
e.foregroundIdle()
e.thought("The child is launched; I can return control to the operator.")
e.text("Foreground response after async launch.")

if !emitCompletion {
return
}
backgroundEmitter := &emitter{ctx: context.Background(), conn: e.conn, sid: e.sid}
go func() {
time.Sleep(d)
// Claude's async Agent invocation is terminal at launch. The later
// completion is a task-notification usage frame, not a second tool update.
backgroundEmitter.completeDetachedWork()
}()
}

// emitDetachedBackgroundWork launches work that outlives this prompt response.
// It is intentionally different from /background, whose foreground prompt stays
// open for the entire delay, so E2E can cover settled+background explicitly.
func emitDetachedBackgroundWork(e *emitter, cmd string) {
d := parseBackgroundDuration(cmd, 8*time.Second)
e.text("Launching detached background work; this foreground turn is complete.")

taskToolID := nextToolID()
e.launchAsyncSubagentTool(taskToolID,
"Detached background exploration",
"Continue working after the foreground response ends",
"general-purpose")

backgroundEmitter := &emitter{ctx: context.Background(), conn: e.conn, sid: e.sid}
go func() {
time.Sleep(d)
backgroundEmitter.completeDetachedWork()
}()
}

// emitSleep sleeps for the requested duration (default 10s) then responds.
// Useful for simulating a slow agent turn without any tool calls.
func emitSleep(e *emitter, cmd string) {
Expand All @@ -291,6 +357,59 @@ func emitSleep(e *emitter, cmd string) {
e.text(fmt.Sprintf("Slept for %s.", d))
}

// emitBackgroundWork reproduces the fine-grained busy signal window
// (ADR-0049): the foreground emits a line, spawns a
// top-level subagent Task that holds the turn open, then goes IDLE for the
// requested duration (default 8s) with no foreground output — the exact state
// where the orchestrator narrows the busy gate so the composer accepts input
// while the session still reads RUNNING. When the hold elapses the subagent
// completes, the foreground resumes, and the turn ends (→ done).
func emitBackgroundWork(e *emitter, cmd string) {
d := parseBackgroundDuration(cmd, 8*time.Second)

e.text("Kicking off background work; I'll keep going in the background.")

taskToolID := nextToolID()
e.startSubagentTool(taskToolID,
"Background exploration",
"Explore the codebase while the foreground stays idle",
"general-purpose")

// Hold the turn open with NO foreground output so the session stays in the
// background-idle substate for the whole window.
time.Sleep(d)

e.completeSubagentTool(taskToolID, "Background work finished", subagentResult{
agentID: "agent_e2e_background",
subagentType: "general-purpose",
durationMs: d.Milliseconds(),
totalTokens: 4242,
toolUseCount: 1,
})

e.text("Background work complete.")
}

// parseBackgroundDuration reads the optional duration argument of a /background
// command, returning def when it is absent or unparseable. A value carrying an
// explicit unit is honored as-is (`1m`, `500ms`, `2h`); a bare number is treated
// as seconds (`8` → 8s). The explicit-unit parse is tried FIRST: appending "s"
// to a unit-bearing value like `1m` would otherwise parse as the valid-but-wrong
// "1ms" (1 millisecond) and never reach the correct interpretation.
func parseBackgroundDuration(cmd string, def time.Duration) time.Duration {
parts := strings.Fields(cmd)
if len(parts) < 2 {
return def
}
if parsed, err := time.ParseDuration(parts[1]); err == nil && parsed > 0 {
return parsed
}
if secs, err := time.ParseDuration(parts[1] + "s"); err == nil && secs > 0 {
return secs
}
return def
}

// emitError emits an error message.
func emitError(e *emitter, model string) {
randomDelay(model)
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/cmd/mock-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ func mockAvailableCommands() []acp.AvailableCommand {
}
return []acp.AvailableCommand{
{Name: "slow", Description: "Run a slow response (default 5s)", Input: hint("duration (e.g. 10s)")},
{Name: "background", Description: "Spawn a subagent and stay foreground-idle (default 8s)", Input: hint("duration (e.g. 8s)")},
{Name: "detached-background", Description: "Launch work that outlives the foreground turn (default 8s)", Input: hint("duration (e.g. 8s)")},
{Name: "async-subagent-lifecycle", Description: "Replay an async Agent lifecycle (default 20s)", Input: hint("duration (e.g. 20s)")},
{Name: "async-subagent-teardown", Description: "Replay async Agent work with a missing completion"},
{Name: "error", Description: "Simulate an error"},
{Name: "overloaded", Description: "Simulate a transient 529 Overloaded error (fails once, then recovers)"},
{Name: "thinking", Description: "Emit thinking/reasoning blocks"},
Expand Down
21 changes: 11 additions & 10 deletions apps/backend/internal/agent/runtime/lifecycle/event_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,17 @@ func (p PrepareCompletedEventPayload) GetSessionID() string {

// AgentStreamEventData contains the nested event data within AgentStreamEventPayload.
type AgentStreamEventData struct {
Type string `json:"type"`
ACPSessionID string `json:"acp_session_id,omitempty"`
Text string `json:"text,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolTitle string `json:"tool_title,omitempty"`
ToolStatus string `json:"tool_status,omitempty"`
Error string `json:"error,omitempty"`
SessionStatus string `json:"session_status,omitempty"` // "resumed" or "new" for session_status events
Data interface{} `json:"data,omitempty"`
Type string `json:"type"`
ACPSessionID string `json:"acp_session_id,omitempty"`
Text string `json:"text,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolTitle string `json:"tool_title,omitempty"`
ToolStatus string `json:"tool_status,omitempty"`
Error string `json:"error,omitempty"`
SessionStatus string `json:"session_status,omitempty"` // "resumed" or "new" for session_status events
PromptGeneration uint64 `json:"prompt_generation,omitempty"`
Data interface{} `json:"data,omitempty"`

// ParentToolCallID identifies the parent Task tool call when this event
// comes from a subagent. Used for visual nesting in the UI.
Expand Down
1 change: 1 addition & 0 deletions apps/backend/internal/agent/runtime/lifecycle/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func (p *EventPublisher) PublishAgentStreamEvent(execution *AgentExecution, even
ToolStatus: event.ToolStatus,
Error: event.Error,
SessionStatus: event.SessionStatus,
PromptGeneration: event.PromptGeneration,
Data: event.Data,
Normalized: event.NormalizedPayload,
AvailableCommands: event.AvailableCommands,
Expand Down
Loading
Loading