Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ changes accumulate. Track in-flight protocol changes via PRs touching
- `JsonPrimitive` type alias (`string | number | boolean | null`) in `types/common/state.ts`.
- `session/activeClientRemoved` action to release a single active client from a
session by `clientId`.
- Canvas surface — `SessionState.canvasRegistry`, `SessionState.openCanvases`, and `SessionState.canvasRequests` state slices, plus `SessionActiveClient.canvasProviders` and `SessionActiveClient.canRenderCanvases`, modelling declared, instanced, agent-openable UI surfaces. See the [Canvases guide](docs/guide/canvases.md).
- Canvas actions (introduced in `0.5.0`): `session/canvasRegistryChanged`, `session/canvasInstanceOpened`, `session/canvasInstanceUpdated`, `session/canvasInstanceClosed`, `session/canvasInstanceCloseRequested`, `session/canvasRequestCreated`, `session/canvasRequestCompleted`, and `session/canvasRequestCancelled`.

### Changed

Expand Down
2 changes: 2 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.
lightweight session-list presentation hints.
- `SessionActiveClientRemovedAction` (wire `session/activeClientRemoved`) to
release a single active client by `ClientId`.
- Canvas surface — `SessionState.CanvasRegistry`, `SessionState.OpenCanvases`, and `SessionState.CanvasRequests`, plus `SessionActiveClient.CanvasProviders` and `SessionActiveClient.CanRenderCanvases`, for declared, instanced, agent-openable UI surfaces.
- Eight canvas actions (wire `session/canvas*`): `SessionCanvasRegistryChangedAction`, `SessionCanvasInstanceOpenedAction`, `SessionCanvasInstanceUpdatedAction`, `SessionCanvasInstanceClosedAction`, `SessionCanvasInstanceCloseRequestedAction`, `SessionCanvasRequestCreatedAction`, `SessionCanvasRequestCompletedAction`, and `SessionCanvasRequestCancelledAction`, applied by `ApplyActionToSession`.

### Changed

Expand Down
131 changes: 131 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -800,10 +800,141 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
return ReduceOutcomeNoOp
case *ahptypes.SessionMcpServerStateChangedAction:
return applyMcpServerStatusChanged(state, a)
case *ahptypes.SessionCanvasRegistryChangedAction:
state.CanvasRegistry = append([]ahptypes.SessionCanvasDeclaration(nil), a.Canvases...)
return ReduceOutcomeApplied
case *ahptypes.SessionCanvasInstanceOpenedAction:
for i := range state.OpenCanvases {
if state.OpenCanvases[i].InstanceId == a.Instance.InstanceId {
state.OpenCanvases[i] = a.Instance
return ReduceOutcomeApplied
}
}
state.OpenCanvases = append(state.OpenCanvases, a.Instance)
return ReduceOutcomeApplied
case *ahptypes.SessionCanvasInstanceUpdatedAction:
if state.OpenCanvases == nil {
return ReduceOutcomeNoOp
}
for i := range state.OpenCanvases {
if state.OpenCanvases[i].InstanceId == a.InstanceId {
if a.Title != nil {
state.OpenCanvases[i].Title = a.Title
}
if a.Status != nil {
state.OpenCanvases[i].Status = a.Status
}
if a.Url != nil {
state.OpenCanvases[i].Url = a.Url
}
if a.Availability != nil {
state.OpenCanvases[i].Availability = *a.Availability
}
return ReduceOutcomeApplied
}
}
return ReduceOutcomeNoOp
case *ahptypes.SessionCanvasInstanceClosedAction:
hadOpen := false
for i := range state.OpenCanvases {
if state.OpenCanvases[i].InstanceId == a.InstanceId {
hadOpen = true
break
}
}
hadRequest := false
for i := range state.CanvasRequests {
if state.CanvasRequests[i].InstanceId == a.InstanceId {
hadRequest = true
break
}
}
if !hadOpen && !hadRequest {
return ReduceOutcomeNoOp
}
if hadOpen {
next := state.OpenCanvases[:0]
for _, c := range state.OpenCanvases {
if c.InstanceId == a.InstanceId {
continue
}
next = append(next, c)
}
if len(next) == 0 {
state.OpenCanvases = nil
} else {
state.OpenCanvases = next
}
}
if hadRequest {
next := state.CanvasRequests[:0]
for _, r := range state.CanvasRequests {
if r.InstanceId == a.InstanceId {
continue
}
next = append(next, r)
}
if len(next) == 0 {
state.CanvasRequests = nil
} else {
state.CanvasRequests = next
}
}
return ReduceOutcomeApplied
case *ahptypes.SessionCanvasInstanceCloseRequestedAction:
// Pure client-to-host signal: the host drives the actual close and
// ultimately a `session/canvasInstanceClosed`.
return ReduceOutcomeNoOp
case *ahptypes.SessionCanvasRequestCreatedAction:
for i := range state.CanvasRequests {
if state.CanvasRequests[i].RequestId == a.Request.RequestId {
state.CanvasRequests[i] = a.Request
return ReduceOutcomeApplied
}
}
state.CanvasRequests = append(state.CanvasRequests, a.Request)
return ReduceOutcomeApplied
case *ahptypes.SessionCanvasRequestCompletedAction:
if !removeCanvasRequest(state, a.RequestId) {
return ReduceOutcomeNoOp
}
return ReduceOutcomeApplied
case *ahptypes.SessionCanvasRequestCancelledAction:
if !removeCanvasRequest(state, a.RequestId) {
return ReduceOutcomeNoOp
}
return ReduceOutcomeApplied
}
return ReduceOutcomeOutOfScope
}

// removeCanvasRequest drops the request keyed by requestId from
// state.CanvasRequests, clearing the slice to nil when it becomes empty so the
// field serialises as absent. Reports whether a matching request was found.
func removeCanvasRequest(state *ahptypes.SessionState, requestID string) bool {
if state.CanvasRequests == nil {
return false
}
found := false
next := state.CanvasRequests[:0]
for _, r := range state.CanvasRequests {
if r.RequestId == requestID {
found = true
continue
}
next = append(next, r)
}
if !found {
return false
}
if len(next) == 0 {
state.CanvasRequests = nil
} else {
state.CanvasRequests = next
}
return true
}

func applyTurnStarted(state *ahptypes.ChatState, a *ahptypes.ChatTurnStartedAction) ReduceOutcome {
state.ActiveTurn = &ahptypes.ActiveTurn{
Id: a.TurnId,
Expand Down
Loading
Loading