// TODO ExceptionState: separate error screen with stack trace

package debugger

import (
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	

	
	
	
	
	
	

	amgraph 
	amhelp 
	am 
	
	
	
	amvis 
)

var _ = ss.ErrGraph

func ( *Debugger) ( *am.Event) bool {
	// ignore graph errs
	return false
}

// TODO Enter

var _ = ss.Start

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Start)
	 := .params.StartupView

	// cview TUI app
	.App = cview.NewApplication()
	if .params.Screen != nil {
		.App.SetScreen(.params.Screen)

		// headless mode
	} else if .params.UiSsh {
		.Mach.EvAdd1(, ss.SshServer, nil)
		.App.SetScreen(tcell.NewSimulationScreen("UTF-8"))
	}

	// forceful race solving
	.App.SetBeforeDrawFunc(func( tcell.Screen) bool {
		// dont draw while transitioning
		 := .Mach.Transition() == nil
		if ! {
			// reschedule this repaint
			// d.Mach.Log("postpone draw")
			.repaintPending.Store(true)
			return true
		}

		// mark as in progress
		.drawing.Store(true)
		return false
	})
	.App.SetAfterDrawFunc(func( tcell.Screen) {
		.drawing.Store(false)
	})
	.App.SetAfterResizeFunc(func( int,  int) {
		.Mach.Go(, func() {
			time.Sleep(time.Millisecond * 300)
			.Mach.Add1(ss.Resized, nil)
		})
	})
	// catch ctrl+c
	// d.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
	// 	if event.Key() == tcell.KeyCtrlC {
	// 		_ = d.Mach.EvAdd1(e, ss.Disposing, nil)
	// 		return nil
	// 	}
	//
	// 	return event
	// })

	// init the rest
	.hBindKeyboard()
	.hInitUiComponents()
	.hInitLayout()
	// d.hUpdateFocusableList()
	if .params.EnableMouse {
		.App.EnableMouse(true)
	}
	if .params.ViewReader {
		.Mach.EvAdd1(, ss.LogReaderEnabled, nil)
	}

	// default filters
	 := S{ss.FilterChecks}
	if .params.Filters.SkipOutGroup {
		 = append(, ss.FilterOutGroup)
	}
	.Mach.EvAdd(, , nil)

	// draw in a goroutine
	.Mach.Fork(, , func() {
		.App.SetRoot(.LayoutRoot, true)
		 := .App.Run()
		if  != nil {
			.Mach.AddErr(, nil)
		}

		.Mach.EvAdd1(, ss.Disposing, nil)
	})

	// post-start ops
	.Mach.Go(, func() {
		// initial view from CLI
		switch  {
		case "tree-matrix":
			.Mach.Add1(ss.TreeMatrixView, nil)
		case "matrix":
			.Mach.Add1(ss.MatrixView, nil)
		}

		// go directly to Ready when data empty
		if len(.Clients) <= 0 {
			.Mach.Add1(ss.Ready, nil)
			return
		}

		// init imported data
		.buildClientList(-1)
		.Mach.Add1(ss.Ready, nil)
	})

	// servers TODO extract, wait for listening

	if .ServerMux != nil {
		.Mach.Go(, func() {
			if  := .ServerMux.Serve();  != nil &&
				!errors.Is(, cmux.ErrListenerClosed) &&
				!errors.Is(, cmux.ErrServerClosed) {

				.Mach.EvAddErr(, , nil)
			}
		})
	}

	if .ServerHttp != nil {
		if .params.UiMcp {
			,  := newMcpServer()
			if  != nil {
				.Mach.Log("Error: %s", )
				return
			}
			.ServerHttp.Handler.(*http.ServeMux).Handle("/mcp", .Http)
		}

		.Mach.Go(, func() {
			if  := .ServerHttp.ListenAndServe();  != nil &&
				 != http.ErrServerClosed {

				.Mach.EvAddErr(, , nil)
			}
		})
	}
}

func ( *Debugger) ( *am.Event) {
	if .App.GetScreen() != nil {
		.App.Stop()
	}
}

var _ = ss.Ready

func ( *Debugger) ( *am.Event) {
	.heartbeatT = time.NewTicker(heartbeatInterval)
	 := .Mach.NewStateCtx(ss.Ready)

	// late options
	// TODO move to hSetParams?
	if .params.ViewNarrow {
		.Mach.EvAdd1(, ss.UserNarrowLayout, nil)
	}
	.hSyncOptsTimelines()
	if .params.OutputDiagrams.Value > 0 {
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)
	}
	if .params.ViewRain {
		.Mach.EvAdd1(, ss.MatrixRain, nil)
	}
	if .params.TailMode {
		.Mach.EvAdd1(, ss.TailMode, nil)
	}

	// TODO merge parsing with addr bar
	if ,  := types.ParseMachUrl(.params.MachUrl);  == nil {
		go .GoToMachAddress(, false)
	}

	// initial focus TODO def focus
	.Mach.EvAdd(, S{ss.AfterFocus, ss.ClientListFocused}, Pass(&A{
		FocusPrimitive: .clientList,
	}))

	// unblock
	.Mach.Fork(, , func() {
		for {
			select {
			case <-.heartbeatT.C:
				.Mach.Add1(ss.Heartbeat, nil)

			case <-.Done():
				.heartbeatT.Stop()
				return
			}
		}
	})

	// select imported client TODO
	if len(.Clients) > 0 &&
		!.Mach.Any1(ss.ClientSelected, ss.SelectingClient) &&
		.params.MachUrl == "" {

		 := .Clients[maps.Keys(.Clients)[0]]
		.Mach.EvAdd1(, ss.SelectingClient, Pass(&A{
			ClientId: .Id,
		}))
	}
}

func ( *Debugger) ( *am.Event) {
	.heartbeatT.Stop()
}

var _ = ss.Heartbeat

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Ready)
	.Mach.EvRemove1(, ss.Heartbeat, nil)
	.Mach.Fork(, , func() {
		amhelp.AskEvAdd1(, .Mach, ss.GcMsgs, nil)
	})
}

var _ = ss.StateNameSelected

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	return .State != ""
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.StateNameSelected)
	.C.SelectedState = am.ParseArgs[A](.Args).State
	.lastSelectedState = .C.SelectedState

	switch .Mach.Switch(states.DebuggerGroups.Views) {

	case ss.TreeLogView:
		.hUpdateSchemaTree()

	case ss.TreeMatrixView:
		.hUpdateSchemaTree()
		.hUpdateMatrix()

	case ss.MatrixView:
		.hUpdateMatrix()
	}

	.hUpdateStatusBar()
	.Mach.Fork(, , func() {
		amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
	})
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Start)
	if .C != nil {
		.C.SelectedState = ""
	}
	.hUpdateSchemaTree()
	.hUpdateStatusBar()
	.Mach.Fork(, , func() {
		amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
	})
}

var _ = ss.Playing

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Playing)
	if .playTimer == nil {
		.playTimer = time.NewTicker(playInterval)
	} else {
		// TODO dont reset if resuming after switching clients
		.playTimer.Reset(playInterval)
	}

	// initial play step
	if .Mach.Is1(ss.TimelineStepsFocused) {
		.Mach.EvAdd1(, ss.FwdStep, nil)
	} else {
		.Mach.EvAdd1(, ss.Fwd, nil)
	}
	.hUpdateToolbar()

	.Mach.Fork(, , func() {
		for .Err() == nil {
			select {
			case <-.Done(): // expired

			case <-.playTimer.C:

				if .Mach.Is1(ss.TimelineStepsFocused) {
					.Mach.Add1(ss.FwdStep, nil)
				} else {
					.Mach.Add1(ss.Fwd, nil)
				}
			}
		}
	})
}

func ( *Debugger) ( *am.Event) {
	.playTimer.Stop()
	.hUpdateToolbar()
}

var _ = ss.Paused

func ( *Debugger) ( *am.Event) {
	// TODO stop scrolling the log when coming from TailMode (confirm)
	.hUpdateTxBars()
	.draw()
}

var _ = ss.TailMode

func ( *Debugger) ( *am.Event) {
	.hSetCursor1(, &A{
		Cursor1:    len(.C.MsgTxs),
		FilterBack: true,
	})
	.hUpdateMatrix()
	.hUpdateClientList()
	.hUpdateToolbar()
	// needed bc tail mode if carried over via SelectingClient
	.hRedrawFull(true)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateMatrix()
	.hUpdateToolbar()
	.hRedrawFull(true)
}

var _ = ss.Redraw

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.Redraw, nil)
	 := am.ParseArgs[A](.Args).Immediate
	.hRedrawFull()
}

// ///// FWD / BACK

var _ = ss.UserFwd

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.UserFwd, nil)
}

var _ = ss.Fwd

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	 := max(.Amount, 1)
	return .C.CursorTx1+ <= len(.C.MsgTxs)
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.Fwd, nil)
	 := .C

	 := am.ParseArgs[A](.Args)
	 := max(.Amount, 1)

	.hSetCursor1(, &A{
		Cursor1: .CursorTx1 + ,
	})
	if .Mach.Is1(ss.Playing) && .CursorTx1 == len(.MsgTxs) {
		.Mach.EvRemove1(, ss.Playing, nil)
	}

	// sidebar for errs
	.hUpdateClientList()
	.hRedrawFull(false)
}

var _ = ss.UserBack

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.UserBack, nil)
}

var _ = ss.Back

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	 := max(.Amount, 1)
	return .C.CursorTx1- >= 0
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.Back, nil)

	 := am.ParseArgs[A](.Args)
	 := max(.Amount, 1)

	.hSetCursor1(, &A{
		Cursor1:    .C.CursorTx1 - ,
		FilterBack: true,
	})

	// sidebar for errs
	.hUpdateClientList()
	.hRedrawFull(false)
}

// ///// STEP BACK / FWD

var _ = ss.UserFwdStep

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.UserFwdStep, nil)
}

var _ = ss.FwdStep

func ( *Debugger) ( *am.Event) bool {
	 := .hNextTx()
	if  == nil {
		return false
	}
	return .C.CursorStep1 < len(.Steps)+1
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.FwdStep, nil)

	// next tx
	 := .hNextTx()
	// scroll to the next tx
	if .C.CursorStep1 == len(.Steps) {
		.Mach.EvAdd1(, ss.Fwd, nil)
		return
	}
	.C.CursorStep1++

	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

var _ = ss.UserBackStep

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.UserBackStep, nil)
}

var _ = ss.BackStep

func ( *Debugger) ( *am.Event) bool {
	return .C.CursorStep1 > 0 || .C.CursorTx1 > 0
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.BackStep, nil)

	// wrap if there's a prev tx
	if .C.CursorStep1 <= 0 {
		.hSetCursor1(, &A{Cursor1: .hPrevTxIdx() + 1})

		.Mach.EvAdd1(, ss.UpdateLogScheduled, nil)
		if  := .hNextTx();  != nil {
			.C.CursorStep1 = len(.Steps)
		}

	} else {
		.C.CursorStep1--
	}

	.updateClientList()
	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

// TODO move
func ( *Debugger) () {
	// TODO merge with a CursorStep setter
	 := .C.CursorStep1 != 0

	if  {
		.Mach.Add1(ss.TimelineStepsScrolled, nil)
	} else {
		.Mach.Remove1(ss.TimelineStepsScrolled, nil)
	}
}

var _ = ss.TimelineStepsScrolled

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

var _ = ss.TimelineStepsFocused

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

var _ = ss.Toolbar1Focused

func ( *Debugger) ( *am.Event) {
	.toolbars[0].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[0].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

var _ = ss.Toolbar2Focused

func ( *Debugger) ( *am.Event) {
	.toolbars[1].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[1].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

var _ = ss.Toolbar3Focused

func ( *Debugger) ( *am.Event) {
	.toolbars[2].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[2].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

var _ = ss.Toolbar4Focused

func ( *Debugger) ( *am.Event) {
	.toolbars[3].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[3].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

var _ = ss.AddressFocused

func ( *Debugger) ( *am.Event) {
	.addressBar.SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.addressBar.SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

var _ = ss.TreeGroupsFocused

func ( *Debugger) ( *am.Event) {
	.treeGroups.SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.treeGroups.SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

// ///// CONNECTION

var _ = ss.ConnectEvent

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	if .MsgStruct == nil || .ConnId == "" || .MsgStruct.ID == "" {
		.Mach.Log("Error: msg_struct malformed\n")
		return false
	}

	return true
}

func ( *Debugger) ( *am.Event) {
	// initial structure data
	 := am.ParseArgs[A](.Args)
	 := .MsgStruct
	 := .ConnId
	var  *Client

	// cleanup removes all previous clients if all are disconnected
	 := false
	if .params.CleanOnConnect {
		// remove old clients
		 = .hCleanOnConnect()
	}

	// update existing client
	if ,  := .Clients[.ID];  {
		if .ConnId != "" && .ConnId ==  {
			.Mach.Log("schema changed for %s", .ID)
			// TODO use MsgStructPatch
			// TODO keep old revisions
			.MsgStruct = 
			 = 
			.ParseSchema()

		} else {
			.Mach.Log("client %s already exists, overriding", .ID)
			.hRemoveClient(.Id)
		}
	}

	// create a new client
	if  == nil {
		 := &server.Exportable{
			MsgStruct: ,
		}
		 = newClient(.ID, , amhelp.SchemaHash(.States), )
		.Connected.Store(true)
		.Clients[.ID] = 
	}

	// re-select the last group
	if  := .lastSelectedGroup;  != "" {
		if ,  := .MsgStruct.Groups[];  {
			.SelectedGroup = 
		}
	}

	if ! {
		.buildClientList(-1)
	}

	// rebuild the UI in case of a cleanup or connect under the same ID
	if  || (.C != nil && .C.Id == .ID) {
		// select the new (and only) client
		.C = 
		.log.Clear()
		.hUpdateTimelines()
		.hUpdateTxBars()
		.hUpdateBorderColor()
		.buildClientList(0)
		// initial build of the schema tree
		.hBuildSchemaTree()
		.hUpdateTreeGroups()
		.hUpdateViews(false)
	}

	// remove the last active client if over the limit
	// TODO prioritize disconns
	if len(.Clients) > maxClients {
		var (
			 time.Time
			   string
		)
		// TODO get time from msgs
		for ,  := range .Clients {
			 := .LastActive()
			if .After() ||  == "" {
				 = 
				 = 
			}
		}
		.Mach.EvAdd1(, ss.RemoveClient, Pass(&A{
			ClientId: ,
		}))
	}

	// if only 1 client connected, select it
	// if the only client in total, select it
	if len(.Clients) == 1 || (.params.SelectConnected &&
		.hConnectedClients() == 1) {

		.Mach.EvAdd1(, ss.SelectingClient, Pass(&A{
			ClientId:      .ID,
			FromConnected: true,
		}))
		.hPrependHistory(&types.MachAddress{MachId: .ID})

		// re-select the state
		if .lastSelectedState != "" {
			.Mach.EvAdd1(, ss.StateNameSelected, Pass(&A{
				State: .lastSelectedState,
			}))
			// TODO Keep in StateNameSelected behind a flag
			.hSelectTreeState(.lastSelectedState)
		}
	}

	// first client, tail mode
	if len(.Clients) == 1 {
		.Mach.EvAdd1(, ss.TailMode, nil)
	}

	// graph
	if .graph != nil {
		_ = .graph.AddClient()
		// TODO errors, check for dups, enable once stable
		// if err != nil {
		// d.Mach.EvAddErr(e, err, nil)
		// }
	}
	.Mach.EvAdd1(, ss.InitClient, Pass(&A{
		Id: .ID,
	}))

	.draw()
}

var _ = ss.DisconnectEvent

func ( *Debugger) ( *am.Event) bool {
	if am.ParseArgs[A](.Args).ConnId == "" {
		.Mach.Log("Error: DisconnectEvent malformed\n")
		return false
	}

	return true
}

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args).ConnId
	for ,  := range .Clients {
		if .ConnId != "" && .ConnId ==  {
			// mark as disconnected
			.Connected.Store(false)
			.Mach.Log("client %s disconnected", .Id)

			// remove pipes from other clients TODO optimize by following pipes
			for ,  := range .Clients {
				// skip empty
				if len(.MsgTxsParsed) == 0 {
					continue
				}

				// TODO create a fake tx, dont overwrite
				 := .MsgTxsParsed[len(.MsgTxsParsed)-1]
				.ReaderEntries = slices.DeleteFunc(.ReaderEntries,
					func( *types.LogReaderEntryPtr) bool {
						 := .GetReaderEntry(.TxId, .EntryIdx)
						return  != nil && .Mach == .Id
					})
			}

			break
		}
	}

	.hUpdateBorderColor()
	.hUpdateAddressBar()
	if .Mach.Is1(ss.FilterDisconn) {
		.buildClientList(-1)
	} else {
		.hUpdateClientList()
	}
	.draw()
}

// ///// CLIENTS

var _ = ss.ClientMsg

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	return .MsgsTx != nil && .ConnIds != nil
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.ClientMsg, nil)

	// TODO make it async via a dedicated goroutine, pushing results to
	//  async multi state ClientMsgDone (if possible)

	 := am.ParseArgs[A](.Args)
	 := .MsgsTx
	 := .ConnIds
	 := .Mach

	// GC in progress - save msgs and parse on next ClientMsgState
	if .Is1(ss.GcMsgs) {
		.msgsDelayed = append(.msgsDelayed, ...)
		.msgsDelayedConns = append(.msgsDelayedConns, ...)

		return
	}

	// parse pending msgs, if any
	if len(.msgsDelayed) > 0 {
		 = slices.Concat(.msgsDelayed, )
		 = slices.Concat(.msgsDelayedConns, )
		.msgsDelayed = nil
		.msgsDelayedConns = nil
	}

	 := false
	 := false
	 := false
	for ,  := range  {

		// TODO check tokens
		 := .MachineID
		 := .Clients[]
		if ,  := .Clients[]; ! {
			.Mach.Log("Error: client not found: %s\n", )
			continue
		}

		if .MsgStruct == nil {
			.Mach.Log("Error: schema missing for %s, ignoring tx\n", )
			continue
		}

		// verify it's from the same client
		if .ConnId != [] {
			.Mach.Log("Error: conn_id mismatch for %s, ignoring tx\n", )
			continue
		}

		// append and parse the msg
		// TODO scalable storage (support filtering)
		 := len(.MsgTxs)
		.MsgTxs = append(.MsgTxs, )
		.hParseMsg(, )
		 := .hFilterTx(, , .filtersFromStates())
		if  {
			.MsgTxsFiltered = append(.MsgTxsFiltered, )
		}

		if  == .C {
			 = true
			 := .hAppendLogEntry()
			if  != nil {
				.Mach.Log("Error: log append %s\n", )
				// d.Mach.EvAddErr(e, err, nil)
				return
			}
			if .Mach.Is1(ss.TailMode) {
				 = true
			}

			// update Tx info on the first Tx
			if len(.MsgTxs) == 1 {
				 = true
			}
		}
	}

	// update graph file
	amgraph.AddErrGraph(, .Mach,
		.hUpdateGraphFile())

	// UI updates for the selected client
	if  {
		// force the latest tx
		.hSetCursor1(, &A{
			Cursor1:    len(.C.MsgTxs),
			FilterBack: true,
		})
		// sidebar for errs
		.hUpdateViews(false)
	}

	// update Tx info on the first Tx
	if  ||  {
		.hUpdateTxBars()
	}

	// timelines always change
	.updateClientList()
	.hUpdateTimelines()
	.hUpdateMatrix()
	.hUpdateAddressBar()

	if  {
		.draw()
	}
}

// TODO move
func ( *Debugger) ( *am.Event) error {
	if !.params.OutputGraph || .graphFileMgml == nil || .graphFileMd == nil {
		return nil
	}

	_ = .graphFileMd.Truncate(0)
	_ = .graphFileMgml.Truncate(0)

	// clone the current graph TODO optimize
	,  := .graph.Clone()
	if  != nil {
		return 
	}
	,  := .Inspect()
	if  != nil {
		return 
	}
	_, _ = .graphFileMd.WriteAt([]byte(amgraph.Markdown()), 0)
	_, _ = .graphFileMgml.WriteAt([]byte(amgraph.Markup()), 0)

	return nil
}

var _ = ss.RemoveClient

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args).ClientId
	,  := .Clients[]

	return  != "" && 
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.RemoveClient, nil)
	 := am.ParseArgs[A](.Args).ClientId
	 := .Clients[]

	// clean up
	delete(.Clients, )
	.hRemoveHistory(.Id)

	// if currently selected, switch to the first one
	if  == .C {
		for  := range .Clients {
			.Mach.EvAdd1(, ss.SelectingClient, Pass(&A{
				ClientId: ,
			}))
			break
		}
		// if last client, unselect
		if len(.Clients) == 0 {
			.Mach.EvRemove1(, ss.ClientSelected, nil)
		}
		.buildClientList(-1)
	} else {
		.buildClientList(.clientList.GetCurrentItemIndex() - 1)
	}

	.draw()
}

var _ = ss.SetGroup

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args).Group
	if  == "" {
		return false
	}

	// extract
	, _, _ = strings.Cut(, ":")

	,  := .C.MsgSchemaParsed.Groups[]
	return  != "" &&  != .C.SelectedGroup && 
}

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args).Group
	 := .C

	if  == "all" {
		.SelectedGroup = ""
	} else {
		.SelectedGroup = strings.Split(, ":")[0]
	}
	.lastSelectedGroup = .SelectedGroup
	.hBuildSchemaTree()
	.hUpdateSchemaTree()
	go amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
	.Mach.EvAdd(, am.S{ss.ToolToggled, ss.UpdateLogScheduled}, Pass(&A{
		FilterTxs:     true,
		LogRebuildEnd: len(.MsgTxs),
	}))
}

var _ = ss.SelectingClient

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args).ClientId
	// same client
	if .C != nil &&  == .C.Id && .Mach.Is1(ss.ClientSelected) {
		return false
	}
	// does the client exist?
	,  := .Clients[]

	return len(.Clients) > 0 &&  != "" && 
}

func ( *Debugger) ( *am.Event) {
	// TODO support tx ID
	 := am.ParseArgs[A](.Args)
	 := .ClientId
	 := .Group
	 := .FromConnected
	 := slices.Contains(.Transition().StatesBefore(), ss.Playing)

	if .Clients[] == nil {
		// TODO handle err, remove state
		.Mach.Log("Error: client not found: %s\n", )

		return
	}

	 := .Mach.NewStateCtx(ss.SelectingClient)
	// select the new default client
	.C = .Clients[]
	// re-feed the whole log and pass the context to allow cancelation
	 := len(.C.LogMsgs)
	// remain in TailMode after the selection
	 := slices.Contains(.Transition().StatesBefore(), ss.TailMode)
	.C.SelectedGroup = 

	// TODO extract SelectingClientFiltered
	// TODO Remove selecting with a timeout (in case it fails)
	.Mach.Fork(, , func() {
		// start with prepping the data
		.hFilterClientTxs()

		// scroll to the same place as the prev client
		// TODO continue in SelectingClientFilteredState
		 := false
		if ! {
			 = .hScrollToTime(, .lastScrolledTxTime, true)
		}

		// or scroll to the last one
		if ! {
			.Mach.Eval("SelectingClientState", func() {
				.hSetCursor1(, &A{
					Cursor1:    len(.C.MsgTxs),
					FilterBack: true,
				})
			}, )
			if .Err() != nil {
				return // expired
			}

		} else {
			// [hSetCursor1] triggers DiagramsScheduled, so do we
			// TODO optimize: push diagram after the initial rendering
			go amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
		}

		// scroll client list item into view
		 := .hGetSidebarCurrClientIdx()
		,  := .clientList.GetOffset()
		, , ,  := .clientList.GetInnerRect()
		if - >  {
			.clientList.SetCurrentItem()
			.updateClientList()
		}

		// rebuild the whole log
		 := am.S{ss.ClientSelected, ss.BuildingLog}
		if  {
			 = append(, ss.TailMode)
		}

		.Mach.EvAdd(, , Pass(&A{
			FromConnected: ,
			FromPlaying:   ,
			LogRebuildEnd: ,
		}))
	})
}

var _ = ss.ClientSelected

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args)
	 := .Mach.NewStateCtx(ss.ClientSelected)
	 := .FromConnected
	 := .FromPlaying

	if .Err() != nil {
		.Mach.Log("Error: context expired\n")
		return // expired
	}

	// catch up with new log msgs
	for  := max(0, .logRebuildEnd-1);  < len(.C.LogMsgs); ++ {
		 := .hAppendLogEntry()
		if  != nil {
			.Mach.Log("Error: log rebuild %s\n", )
		}
	}

	// initial build of the schema tree
	.hBuildSchemaTree()
	.hUpdateTreeGroups()
	.hUpdateTimelines()
	.hUpdateTxBars()
	.hUpdateClientList()
	if .Mach.Is1(ss.TreeLogView) || .Mach.Is1(ss.TreeMatrixView) {
		.hUpdateSchemaTree()
	}

	// update views
	if .Mach.Is1(ss.TreeLogView) {
		.Mach.EvAdd1(, ss.UpdateLogScheduled, nil)
	}
	if .Mach.Is1(ss.MatrixView) || .Mach.Is1(ss.TreeMatrixView) {
		.hUpdateMatrix()
	}

	// first client connected, set tail mode
	if  && len(.Clients) == 1 {
		.Mach.EvAdd1(, ss.TailMode, nil)
	} else if  {
		.Mach.EvAdd1(, ss.Playing, nil)
	}

	// re-select the state
	if .lastSelectedState != "" {
		.Mach.EvAdd1(, ss.StateNameSelected, Pass(&A{
			State: .lastSelectedState,
		}))
		// TODO Keep in StateNameSelected behind a flag
		.hSelectTreeState(.lastSelectedState)
	}

	.hUpdateBorderColor()
	.hUpdateAddressBar()
	.draw()
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.Index1(ss.SelectingClient)
	// clean up, except when switching to SelectingClient
	if !.Mutation().IsCalled() {
		.C = nil
	}

	.log.Clear()
	.treeRoot.ClearChildren()
	.hRedrawFull(true)
}

var _ = ss.HelpDialog

func ( *Debugger) ( *am.Event) {
	// re-render for mem stats
	.hUpdateHelpDialog()
	.hUpdateFocusableList()
	.hUpdateToolbar()
	// TODO use Visibility instead of SendToFront
	.LayoutRoot.SendToFront("main")
	.LayoutRoot.SendToFront(DialogHelp)
	.Mach.EvAdd1(, ss.AfterFocus, Pass(&A{
		FocusPrimitive: .helpDialogLeft,
	}))
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.DialogFocused, nil)
}

var _ = ss.ExportDialog

func ( *Debugger) ( *am.Event) {
	// TODO use Visibility instead of SendToFront
	.LayoutRoot.SendToFront("main")
	.LayoutRoot.SendToFront(DialogExport)
	.Mach.EvAdd(, am.S{ss.UpdateFocus, ss.DialogFocused}, nil)
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.DialogFocused, nil)
}

var _ = ss.DialogFocused

func ( *Debugger) ( *am.Event) {
	 := .Transition()
	 := am.StatesDiff(states.DebuggerGroups.Dialog, .TargetStates())
	if len() == len(states.DebuggerGroups.Dialog) {
		// all dialogs closed, show main
		.LayoutRoot.SendToFront("main")
		.hUpdateToolbar()

		// focus prev one
		,  := .hBoxFromPrimitive(.preModalFocus)
		if  != "" {
			.Mach.EvAdd(, am.S{ss.UpdateFocus, }, nil)
		}

		.draw()
	}
}

var _ = ss.MatrixView

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

var _ = ss.TreeMatrixView

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

var _ = ss.MatrixRain

func ( *Debugger) ( *am.Event) bool {
	 := .Transition().TimeIndexAfter()

	return .Any1(ss.TreeMatrixView, ss.MatrixView) &&
		.Is1(ss.ClientSelected)
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

var _ = ss.ScrollToTx

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args)
	,  := .CursorTx1, .TxId
	 := .C

	return  != nil && ( != "" && .TxIndex() > -1 ||
		 > 0 && len(.MsgTxs) >= )
}

// ScrollToTxState scrolls to a specific transition (cursor position 1-based).

func ( *Debugger) ( *am.Event) {
	defer .Mach.EvRemove1(, ss.ScrollToTx, nil)
	 := am.ParseArgs[A](.Args)
	 := .CursorTx1
	 := .CursorStep1
	 := .TrimHistory

	if .TxId != "" {
		 = .C.TxIndex(.TxId) + 1
	}

	.hSetCursor1(, &A{
		Cursor1:     ,
		CursorStep1: ,
		TrimHistory: ,
	})
	.updateClientList()
	.hRedrawFull(false)
}

var _ = ss.NarrowLayout

func ( *Debugger) ( *am.Event) bool {
	// always allow to exit
	 := .Transition().TimeIndexAfter()
	if .Not1(ss.Start) {
		return true
	}

	return .Not1(ss.UserNarrowLayout)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateToolbar()
	.hUpdateLayout()
	.buildClientList(-1)
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.Mach.EvAdd1(, ss.ClientListVisible, nil)
}

var _ = ss.ScrollToStep

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args).CursorStep1
	 := .C
	return  != nil &&  > 0 && .hNextTx() != nil
}

// ScrollToStepState scrolls to a specific transition (cursor position 1-based).

func ( *Debugger) ( *am.Event) {
	// TODO multi?
	.Mach.EvRemove1(, ss.ScrollToStep, nil)

	 := am.ParseArgs[A](.Args).CursorStep1
	 := .hNextTx()

	if  > len(.Steps) {
		 = len(.Steps)
	}
	.C.CursorStep1 = 

	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

var _ = ss.ToggleTool

func ( *Debugger) ( *am.Event) bool {
	return am.ParseArgs[A](.Args).ToolName.Value != ""
}

func ( *Debugger) ( *am.Event) {
	// TODO split the state into an async one
	// TODO refac to FilterToggledState
	 := am.ParseArgs[A](.Args).ToolName

	// tool is a filter and needs re-filter txs
	 := false
	 := false

	switch  {
	// TODO move logic after toggle to handlers

	case types.ToolFilterCanceledTx:
		.Mach.EvToggle1(, ss.FilterCanceledTx, nil)
		 = true

	case types.ToolFilterQueuedTx:
		.Mach.EvToggle1(, ss.FilterQueuedTx, nil)
		 = true

	case types.ToolFilterAutoTx:
		.Mach.EvToggle1(, ss.FilterAutoTx, nil)
		switch .Mach.Switch(am.S{ss.FilterAutoTx, ss.FilterAutoCanceledTx}) {
		case ss.FilterAutoTx:
			.Mach.EvAdd1(, ss.FilterAutoCanceledTx, nil)
		case ss.FilterAutoCanceledTx:
			.Mach.EvRemove1(, ss.FilterAutoTx, nil)
		default:
			.Mach.EvAdd1(, ss.FilterAutoTx, nil)
		}
		 = true

	case types.ToolFilterEmptyTx:
		.Mach.EvToggle1(, ss.FilterEmptyTx, nil)
		 = true

	case types.ToolFilterHealth:
		.Mach.EvToggle1(, ss.FilterHealth, nil)
		 = true

	case types.ToolFilterOutGroup:
		.Mach.EvToggle1(, ss.FilterOutGroup, nil)
		 = true

	case types.ToolFilterChecks:
		.Mach.EvToggle1(, ss.FilterChecks, nil)
		 = true

	case types.ToolFilterRpcMachs:
		.Mach.EvToggle1(, ss.FilterRpcMachs, nil)
		 = true

	case types.ToolFilterDisconn:
		.Mach.EvToggle1(, ss.FilterDisconn, nil)
		 = true

	case types.ToolNarrowLayout:
		if .Mach.Is1(ss.UserNarrowLayout) {
			.Mach.EvRemove(, S{ss.UserNarrowLayout, ss.NarrowLayout}, nil)
		} else {
			.Mach.EvAdd1(, ss.UserNarrowLayout, nil)
		}

	case types.ToolLogTimestamps:
		.Mach.EvToggle1(, ss.LogTimestamps, nil)
		 = true

	case types.ToolFilterTraces:
		.Mach.EvToggle1(, ss.FilterTraces, nil)

	case types.ToolLog:
		.params.Filters.LogLevel = (.params.Filters.LogLevel + 1) % 6
		.hUpdateSchemaLogGrid()

	case types.ToolDiagrams:
		switch .params.OutputDiagrams {
		case types.ParamsOutputDiagramsNone:
			.params.OutputDiagrams = types.ParamsOutputDiagramsOne
		case types.ParamsOutputDiagramsOne:
			.params.OutputDiagrams = types.ParamsOutputDiagramsTwo
		case types.ParamsOutputDiagramsTwo:
			.params.OutputDiagrams = types.ParamsOutputDiagramsThree
		case types.ParamsOutputDiagramsThree:
			.params.OutputDiagrams = types.ParamsOutputDiagramsNone
		}
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)

	case types.ToolDiagramsSteps:
		.params.OutputTx = !.params.OutputTx
		if .params.OutputTx {
			.hInitOutputTxFiles()
			if  := .hCurrentTx();  != nil {
				.hGenSeqDiagram(am.EvToCtx(.Mach.Context(), ), ,
					.C.MsgTxsParsed[.C.CursorTx1-1])
			}
		} else {
			.hCloseOutputTxFiles()
		}

	case types.ToolDiagramsTx:
		switch .params.OutputDiagTx {
		case types.ParamsOutDiagTxNone:
			.params.OutputDiagTx = types.ParamsOutDiagTxCalled
		case types.ParamsOutDiagTxCalled:
			.params.OutputDiagTx = types.ParamsOutDiagTxMutated
		case types.ParamsOutDiagTxMutated:
			.params.OutputDiagTx = types.ParamsOutDiagTxTouched
		case types.ParamsOutDiagTxTouched:
			.params.OutputDiagTx = types.ParamsOutDiagTxRelations
		case types.ParamsOutDiagTxRelations:
			.params.OutputDiagTx = types.ParamsOutDiagTxNone
		}
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)

	case types.ToolDiagramsGroup:
		switch .params.OutputDiagGroup {
		case types.ParamsOutDiagGroupNone:
			.params.OutputDiagGroup = types.ParamsOutDiagGroupHide
		case types.ParamsOutDiagGroupHide:
			.params.OutputDiagGroup = types.ParamsOutDiagGroupSkip
		case types.ParamsOutDiagGroupSkip:
			.params.OutputDiagGroup = types.ParamsOutDiagGroupNone
		}
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)

	case types.ToolCallLog:
		.params.OutputCallLog = !.params.OutputCallLog
		if !.params.OutputCallLog {
			.hCloseOutputCallLogFiles()
		} else {
			// process whole call log
		}

	case types.ToolOutputLog:
		.params.OutputLog = !.params.OutputLog
		if !.params.OutputLog {
			.hCloseOutputLog()
		} else {
			.hCreateOutputLogFile()
			.Mach.EvAddErr(, .outputLogFile(.Mach.Context()), nil)
		}

	case types.ToolTimelines:
		switch .params.ViewTimelines {
		case types.ParamsViewTimelinesNone:
			.params.ViewTimelines = types.ParamsViewTimelinesOne
		case types.ParamsViewTimelinesOne:
			.params.ViewTimelines = types.ParamsViewTimelinesTwo
		case types.ParamsViewTimelinesTwo:
			.params.ViewTimelines = types.ParamsViewTimelinesNone
		}
		.hSyncOptsTimelines()

	case types.ToolReader:
		if .Mach.Is1(ss.LogReaderEnabled) &&
			.Mach.Any1(ss.MatrixView, ss.TreeMatrixView) {

			.Mach.EvAdd1(, ss.TreeLogView, nil)
		} else if .Mach.Not1(ss.LogReaderEnabled) {
			.Mach.EvAdd1(, ss.LogReaderEnabled, nil)
		} else {
			.Mach.EvRemove1(, ss.LogReaderEnabled, nil)
		}

	case types.ToolRain:
		.Mach.EvAdd1(, ss.ToolRain, nil)

	case types.ToolLogWrap:
		.params.ViewLogWrap = !.params.ViewLogWrap
		.log.SetWrap(.params.ViewLogWrap)

	case types.ToolHelp:
		.Mach.EvToggle1(, ss.HelpDialog, nil)

	case types.ToolPlay:
		if .Mach.Is1(ss.Paused) {
			.Mach.EvAdd1(, ss.Playing, nil)
		} else {
			.Mach.EvAdd1(, ss.Paused, nil)
		}

	case types.ToolTail:
		.Mach.EvToggle1(, ss.TailMode, nil)

	case types.ToolWeb:
		go func() {
			 := openURL("http://" + .params.AddrHttp)
			.Mach.EvAddErr(, , nil)
		}()

	case types.ToolPrev:
		.Mach.EvAdd1(, ss.UserBack, nil)

	case types.ToolNext:
		.Mach.EvAdd1(, ss.UserFwd, nil)

	case types.ToolNextStep:
		.Mach.EvAdd1(, ss.UserFwdStep, nil)

	case types.ToolPrevStep:
		.Mach.EvAdd1(, ss.UserBackStep, nil)

	case types.ToolJumpPrev:
		// TODO state
		go .hJumpBackKey(nil)

	case types.ToolNextClient:
		.hSwitchClient(, 1)

	case types.ToolPrevClient:
		.hSwitchClient(, -1)

	case types.ToolJumpNext:
		// TODO state
		go .hJumpFwdKey(nil)

	case types.ToolFirst:
		.hToolFirstTx()

	case types.ToolLast:
		.hToolLastTx()

	case types.ToolExpand:
		// TODO refresh toolbar on focus changes, reflect expansion state
		.hToolExpand()

	case types.ToolMatrix:
		.toolMatrix()

	case types.ToolExport:
		.Mach.EvAdd(, am.S{ss.ExportDialog, ss.DialogFocused}, nil)
	}

	.Mach.EvAdd1(, ss.ToolToggled, Pass(&A{
		FilterTxs:       ,
		BuildClientList: ,
	}))
}

var _ = ss.ToolToggled

func ( *Debugger) ( *am.Event) {
	defer .Mach.EvRemove1(, ss.ToolToggled, nil)
	 := am.ParseArgs[A](.Args)
	 := .FilterTxs
	 := .BuildClientList

	if  {
		.hFilterClientTxs()
	}

	// TODO skip on dialogs
	if .C != nil {

		// TODO scroll the log to prev position

		// stay on the last one
		if .Mach.Is1(ss.TailMode) {
			.hSetCursor1(, &A{
				Cursor1:    len(.C.MsgTxs),
				FilterBack: true,
			})
		}

		// rebuild the whole log to reflect the UI changes
		.Mach.EvAdd1(, ss.BuildingLog, Pass(&A{
			LogRebuildEnd: len(.C.MsgTxs),
		}))
		// TODO optimization: param to avoid this
		.Mach.EvAdd1(, ss.UpdateLogScheduled, nil)

		if  {
			.hSetCursor1(, &A{
				Cursor1:    .C.CursorTx1,
				FilterBack: true,
			})
		}
	}

	if  {
		// TODO immediate via i
		.buildClientList(-1)
	} else {
		.updateClientList()
	}
	.hUpdateToolbar()
	.hUpdateTimelines()
	.hUpdateMatrix()
	.hUpdateTxBars()
	.draw()
}

var _ = ss.SwitchingClientTx

func ( *Debugger) ( *am.Event) {
	 := .Mach
	 := am.ParseArgs[A](.Args)
	 := .ClientId
	 := .CursorTx1
	 := .Mach.NewStateCtx(ss.SwitchingClientTx)

	.Mach.Fork(, , func() {
		if .C != nil && .C.Id !=  {
			amhelp.EvAdd1Async(, , , ss.ClientSelected,
				ss.SelectingClient, Pass(&A{
					ClientId: ,
				}))
			if .Err() != nil {
				return // expired
			}
		}

		amhelp.EvAdd1Sync(, , , ss.ScrollToTx, Pass(&A{
			CursorTx1:   ,
			TrimHistory: true,
		}))
		if .Err() != nil {
			return // expired
		}

		.Mach.Add1(ss.SwitchedClientTx, nil)
	})
}

var _ = ss.SwitchedClientTx

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.SwitchedClientTx, nil)
}

var _ = ss.ScrollToMutTx

func ( *Debugger) ( *am.Event) {
	.Mach.EvRemove1(, ss.ScrollToMutTx, nil)

	// TODO validate in Enter
	 := am.ParseArgs[A](.Args)
	 := .State
	 := .Fwd

	 := .C
	if  == nil {
		return
	}
	 := -1
	if  {
		 = 1
	}

	for  := .CursorTx1 + ;  > 0 &&  < len(.MsgTxs)+1;  =  +  {

		 :=  - 1
		 := .MsgTxsParsed[]
		 := .MsgTxs[]

		// check mutations and canceled
		if !slices.Contains(.IndexesToStates(.StatesAdded), ) &&
			!slices.Contains(.IndexesToStates(.StatesRemoved), ) &&
			!slices.Contains(.CalledStateNames(.MsgStruct.StatesIndex), ) {

			continue
		}

		// skip filtered out
		if .filtersActive() && !slices.Contains(.MsgTxsFiltered, ) {
			continue
		}

		// scroll to this tx
		.Mach.EvAdd1(, ss.ScrollToTx, Pass(&A{
			CursorTx1:   ,
			TrimHistory: true,
		}))
		break
	}
}

var _ = ss.Exception

func ( *Debugger) ( *am.Event) bool {
	// ignore eval timeouts, but log them
	 := am.ParseArgs[am.AException](.Args)
	if errors.Is(.Err, am.ErrEvalTimeout) {
		if !.IsCheck {
			.Mach.Log(.Err.Error())
		}

		return false
	}

	return true
}

// ExceptionState creates a log file with the error and stack trace, after
// calling the super exception handler.
func ( *Debugger) ( *am.Event) {
	.ExceptionHandler.ExceptionState()
	 := am.ParseArgs[am.AException](.Args)

	if .Mach != nil {
		.hUpdateBorderColor()
	}

	// create / append the err log file
	 := fmt.Sprintf("\n\n%s\n%s\n\n%s", time.Now(), .Err, .ErrTrace)
	 := filepath.Join(.params.OutputDir, "am-dbg-err.log")
	,  := os.OpenFile(, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
	if  != nil {
		.Mach.Log("Error: %s\n", )
		return
	}
	_,  = .Write([]byte())
	if  := .Close();  != nil &&  == nil {
		.Mach.Log("Error: %s\n", )
		return
	}
}

var _ = ss.GcMsgs

func ( *Debugger) ( *am.Event) bool {
	return AllocMem() > uint64(.params.MaxMemMb)*1024*1024
}

func ( *Debugger) ( *am.Event) {
	// TODO GC log reader entries
	// TODO GC tx steps before GCing transitions
	defer .Mach.EvRemove1(, ss.GcMsgs, nil)
	 := .Mach.NewStateCtx(ss.GcMsgs)

	// unblock

	// get oldest clients
	 := maps.Values(.Clients)
	slices.SortFunc(, func(,  *Client) int {
		if len(.MsgTxs) == 0 || len(.MsgTxs) == 0 {
			return 0
		}
		if (*.MsgTxs[0].Time).After(*.MsgTxs[0].Time) {
			return 1
		} else {
			return -1
		}
	})

	runtime.GC()
	 := AllocMem()
	.Mach.Log(.P.Sprintf("Alloc mem: %d MBs\n", /1024/1024))

	// check TTL of client log msgs >lvl 2
	// TODO remember the tip of cleaning (date) and binary find it, then
	//  continue
	for ,  := range  {
		for ,  := range .LogMsgs {
			 := .MsgTxs[].Time
			if .Add(.params.LogOpsTtl).After(time.Now()) {
				continue
			}

			// TODO optimize
			var  []*am.LogEntry
			for ,  := range  {
				if  == nil {
					continue
				}
				if .Level <= am.LogChanges {
					 = append(, )
				}
			}

			// override
			.LogMsgs[] = 
		}
	}

	runtime.GC()
	 := AllocMem()
	if  >  {
		.Mach.Log(.P.Sprintf("GC logs shaved %d MBs\n", (-)/1024/1024))
	}

	 := 0
	for AllocMem() > uint64(.params.MaxMemMb)*1024*1024 {
		if .Err() != nil {
			.Mach.Log("GC: context expired")
			break
		}
		if  > 100 {
			.Mach.EvAddErr(, errors.New("too many GC rounds"), nil)
			break
		}
		.Mach.Log("GC tx round %d", )
		++

		// delete a half per client
		for ,  := range  {
			if .Err() != nil {
				.Mach.Log("GC: context expired")
				break
			}
			 := len(.MsgTxs) / 2
			.MsgTxs = .MsgTxs[:]
			.MsgTxsParsed = .MsgTxsParsed[:]
			.LogMsgs = .LogMsgs[:]

			// empty cache
			.ClearCache()
			// TODO GC c.logReader
			// TODO refresh c.errors (extract from hParseMsg)s

			// adjust the current client
			if .C ==  {

				// rebuild the whole log
				.Mach.EvAdd1(, ss.BuildingLog, Pass(&A{
					LogRebuildEnd: len(.MsgTxs),
				}))
				.CursorTx1 = int(math.Max(0, float64(.CursorTx1-)))
				// re-filter
				if .filtersActive() {
					.hFilterClientTxs()
				}
			}

			// delete small clients
			if len(.MsgTxs) < msgMaxThreshold {
				.Mach.EvAdd1(, ss.RemoveClient, Pass(&A{
					ClientId: .Id,
				}))
			} else {
				.MTimeSum = 0
				for ,  := range .MsgTxsParsed {
					.MTimeSum += .TimeSum
				}
			}
		}

		runtime.GC()
	}
	 := AllocMem()
	if  >  {
		.Mach.Log(.P.Sprintf("GC in total shaved %d MBs", (-)/1024/1024))
	}

	.hRedrawFull(false)
}

var _ = ss.LogReaderVisible

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
}

// TODO remove?
var _ = ss.SetCursor

func ( *Debugger) ( *am.Event) {
	.hSetCursor1(, am.ParseArgs[A](.Args))
}

// func (d *Debugger) CursorSetState(e *am.Event) {
// }

var _ = ss.DiagramsScheduled

func ( *Debugger) ( *am.Event) bool {
	// TODO refuse on too many ErrDiagrams, remove ErrDiagrams in ErrDiagramsState
	return .C != nil && .params.OutputDiagrams != types.ParamsOutputDiagramsNone
}

func ( *Debugger) ( *am.Event) {
	// TODO cancel rendering on:
	//  - client change
	//  - details change
	//  - but not on tx change (wait until completed)
	.Mach.EvAdd1(, ss.DiagramsRendering, nil)
}

var _ = ss.DiagramsRendering

func ( *Debugger) ( *am.Event) bool {
	return .params.OutputDiagrams.Value > 0 && .C != nil
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.DiagramsRendering)
	 := .params.OutputDiagrams.Value
	 := path.Join(.params.OutputDir, "diagrams")
	 := .C
	 := .hCurrentTx()
	 := fmt.Sprintf("%s-%d-%s", .Id, , .SchemaHash)

	// state groups
	var  S
	if  := .SelectedGroup;  != "" &&
		.params.OutputDiagGroup == types.ParamsOutDiagGroupSkip {

		 = .MsgSchemaParsed.Groups[]
		 = fmt.Sprintf("%s-%s-%d-%s",
			.Id, types.NormalizeGroupName(), , .SchemaHash)
	}
	 := filepath.Join(, +".svg")

	// output dir
	if  := os.MkdirAll(, 0o755);  != nil {
		.Mach.EvAddErrState(, ss.ErrDiagrams,
			fmt.Errorf("create output dir: %w", ), nil)
	}

	// cached?

	// build update fragment
	 := .MsgStruct.StatesIndex
	 := &amvis.Filters{
		MachId:   .Id,
		Index:    ,
		Selected: am.S{.C.SelectedState},
	}
	if  != nil {
		.Active = .ActiveStates()

		// dim
		if .params.OutputDiagTx != types.ParamsOutDiagTxNone {
			 := .MsgTxsParsed[.CursorTx1-1]
			 := .CalledStatesIdxs
			switch .params.OutputDiagTx {
			case types.ParamsOutDiagTxMutated:
				 = slices.Concat(.StatesAdded, .StatesRemoved)
			case types.ParamsOutDiagTxTouched:
				fallthrough
			case types.ParamsOutDiagTxRelations:
				 = .StatesTouched
			}
			.Highlighted = .IndexesToStates()
		}

		// collect touched rels TODO add to step timeline 1-by-1
		if .params.OutputDiagTx == types.ParamsOutDiagTxRelations {
			for ,  := range .Steps {
				if .Type != am.StepRelation {
					continue
				}

				.HighlightedRels = append(.HighlightedRels,
					[3]string{
						.GetFromState(),
						.GetToState(),
						.RelType.String(),
					})
			}
		}
	}
	if .params.OutputDiagGroup == types.ParamsOutDiagGroupHide &&
		.SelectedGroup != "" {

		.Visible = .MsgSchemaParsed.Groups[.SelectedGroup]
	}

	// mem cache
	if .cache.diagramName ==  {
		 := .cache.diagramDom
		.Mach.Fork(, , func() {
			.diagramsMemCache(am.EvToCtx(, ), , ,
				, )
		})
		return

		// file cache, move to mem cache
	} else if ,  := os.Stat();  == nil {
		.Mach.Fork(, , func() {
			.diagramsFileCache(am.EvToCtx(, ), , ,
				, )
		})
		return
	}

	// no cache - render

	// clone the current graph TODO optimize
	,  := .graph.Clone()
	if  != nil {
		.Mach.EvAddErrState(, ss.ErrDiagrams, , nil)
		return
	}

	// unblock
	.Mach.EvAdd1(, ss.DiagramsNoCache, nil)
	.Mach.Fork(, , func() {
		.diagramsRender(am.EvToCtx(, ), , .Id, , ,
			len(.Clients), , , )
	})
}

var _ = ss.DiagramsReady

func ( *Debugger) ( *am.Event) {
	defer .Mach.EvRemove1(, ss.DiagramsReady, nil)

	// update cache
	 := am.ParseArgs[A](.Args)
	if .DiagramCache != nil {
		.cache.diagramDom = .DiagramCache
		.cache.diagramName = .DiagramName
	}

	// render a fresher one, if scheduled
	.genGraphsLast = time.Now()
	if .Mach.Is1(ss.DiagramsScheduled) {
		.Mach.EvRemove1(, ss.DiagramsScheduled, nil)
		.Mach.EvAdd1(, ss.DiagramsRendering, nil)
	}
}

var _ = ss.ClientListVisible

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.ClientListVisible)
	// TODO via relations
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
	.buildClientList(-1)
	.hUpdateLayout()
	.Mach.Fork(, , func() {
		if !amhelp.Wait(, sidebarUpdateDebounce) {
			return
		}
		.drawClientList()
	})
}

func ( *Debugger) ( *am.Event) {
	// TODO via relations
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
}

var _ = ss.TimelineTxHidden

func ( *Debugger) ( *am.Event) {
	// handled in TimelineStepsHiddenState
	if .Machine().Is1(ss.TimelineStepsHidden) {
		return
	}

	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

var _ = ss.TimelineStepsHidden

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

var _ = ss.UpdateFocus

func ( *Debugger) ( *am.Event) {
	 := .focusablePrims
	.hUpdateFocusableList()
	.hUpdateBorderColor()

	var  cview.Primitive
	// change focus (or not) when changing view types
	switch .Mach.Switch(states.DebuggerGroups.Focused) {
	case ss.ClientListFocused:
		 = .clientList
	case ss.AddressFocused:
		 = .addressBar
	case ss.TreeFocused:
		 = .tree
	case ss.TreeGroupsFocused:
		 = .treeGroups
	case ss.LogFocused:
		 = .log
	case ss.LogReaderFocused:
		 = .logReader
	case ss.MatrixFocused:
		 = .matrix
	case ss.TimelineTxsFocused:
		 = .timelineTxs
	case ss.TimelineStepsFocused:
		 = .timelineSteps
	case ss.Toolbar1Focused:
		 = .toolbars[0]
	case ss.Toolbar2Focused:
		 = .toolbars[1]
	case ss.Toolbar3Focused:
		 = .toolbars[2]
	case ss.Toolbar4Focused:
		 = .toolbars[3]
	case ss.DialogFocused:
		switch {
		case .Mach.Is1(ss.HelpDialog):
			 = .helpDialogLeft
		case .Mach.Is1(ss.ExportDialog):
			 = .exportDialog
		}

	// layout changed, focus the nearest
	default:
		 := slices.Index(, .Focused)
		 := min(max(-1, 0), len(.focusablePrims)-1)
		var  string
		,  = .hBoxFromPrimitive(.focusablePrims[])
		// fix state
		.Mach.EvAdd1(, , nil)
	}
	.App.SetFocus()
}

var _ = ss.AfterFocus

func ( *Debugger) ( *am.Event) bool {
	 := am.ParseArgs[A](.Args).FocusPrimitive
	if  == nil {
		return false
	}

	,  := .hBoxFromPrimitive()

	// skip when focus impossible
	return slices.Contains(.focusable, )
}

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args)
	 := .FocusPrimitive
	 := .MouseFocus
	.GetFocusable()

	.Focused = 
	if .Mach.Not1(ss.DialogFocused) {
		.preModalFocus = 
	}

	// correct state from mouse focus
	if  {
		,  := .hBoxFromPrimitive()
		.Mach.EvAdd1(, , nil)
	}

	// update the log highlight on focus change
	if .Mach.Is1(ss.TreeLogView) && .Mach.Not1(ss.LogReaderFocused) {
		.Mach.EvAdd1(, ss.UpdateLogScheduled, nil)
	}

	.hUpdateClientList()
	.hUpdateStatusBar()
	.hUpdateTimelines()
}

var _ = ss.ToolRain

func ( *Debugger) ( *am.Event) {
	if .Mach.Is1(ss.MatrixRain) {
		.Mach.EvAdd1(, ss.TreeLogView, nil)
	} else {
		.Mach.EvAdd(, am.S{ss.MatrixRain, ss.TreeMatrixView}, nil)
		// TODO force redraw to get rect size, not ideal
		.redrawCallback = func() {
			time.Sleep(16 * time.Millisecond)
			.Mach.Eval("ToolRainState", func() {
				.hDrawViews()
			}, nil)
		}
	}
}

// AnyEnter prevents most of mutations during a UI redraw (and vice versa)
// forceful race solving
var _ = am.StateAny

func ( *Debugger) ( *am.Event) bool {
	// always pass network traffic
	 := .Mach
	 := .Mutation()
	 := .CalledIndex(ss.Names())
	 := S{
		ss.ClientMsg, ss.ConnectEvent, ss.DisconnectEvent,
		am.StateException,
	}
	if .Any1(...) {
		return true
	}

	// dont allow mutations while drawing, pull 10 times
	 := .HandlerTimeout / 10
	 := 100
	// compensate extended timeouts
	if amhelp.IsDebug() {
		 = 10 * time.Millisecond
	}
	for .IsValid() {
		 := !.drawing.Load()
		if  {
			// ok
			return true
		}

		// delay, but avoid the race detector which gets stuck here
		if !.params.RaceDetector {
			time.Sleep()
		}

		--
		if  <= 0 {
			// not ok
			break
		}
	}

	// prepend this mutation to the queue and try again TODO loop guard
	// d.Mach.Log("postpone mut")
	go .PrependMut(.Clone())

	return false
}

// AnyState is a global final handler
var _ = am.StateAny

func ( *Debugger) ( *am.Event) {
	 := .Transition()

	// redraw on auto states
	// TODO this should be done better
	if .IsAuto() && .IsAccepted.Load() {
		.repaintPending.Store(false)
		.hUpdateTxBars()
		.draw()
	} else if .repaintPending.Swap(false) {
		.draw()
	}
}

var _ = ss.WebReq

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args)
	 := .HttpRequest
	 := .HttpResponseWriter
	 := .DoneChan
	defer close()

	 := .RequestURI
	switch {

	// diagram viewer
	case  == "/":
		fallthrough
	case  == "/diagrams/mach":
		 := string(amvis.HtmlDiagram)
		 = strings.ReplaceAll(, "localhost:6831",
			.listenHost+":"+.httpPort)
		,  := .Write([]byte())
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)

	// default svg symlink
	case strings.HasPrefix(, "/diagrams/mach.svg"):
		 := filepath.Join(.params.OutputDir, "diagrams", "am-vis.svg")
		,  := os.ReadFile()
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)
		if  != nil {
			return
		}

		// send
		.Header().Set("Content-Type", "image/svg+xml")
		_,  = .Write()
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)
	}
}

type WsDiagMsg struct {
	Type  string
	Addr  *types.MachAddress
	Group string
}

var _ = ss.WebSocketDiag

func ( *Debugger) ( *am.Event) {
	 := .Mach
	 := .NewStateCtx(ss.WebSocketDiag)
	 := am.ParseArgs[A](.Args)
	 := .WebSocketConn
	 := .HttpRequest
	 := .DoneChan
	 := make(chan struct{})

	.EvAdd1(, ss.DiagramsScheduled, nil)

	// unblock
	.Fork(, , func() {
		defer close()
		var  context.Context
		var  context.CancelFunc
		for {
			// release prev run's wait chans
			if  != nil {
				()
			}

			,  = context.WithCancel()
			defer ()

			// wait for diagrams start
			select {
			case <-:
				return
			case <-.When1(ss.DiagramsScheduled, ):
			}

			// show progress
			select {

			case <-:
				return

			case <-.When1(ss.DiagramsNoCache, ):
				// msg
				,  := json.Marshal(WsDiagMsg{
					Type: "loading",
				})
				if  != nil {
					.Log(.Error())
					continue
				}

				// send
				 = .Write(.Context(), websocket.MessageText, )
				if  != nil {
					.EvAddErrState(, ss.ErrWeb, , nil)
					return
				}

			case <-.When1(ss.DiagramsReady, ):
				// ok
			}

			// wait for diagrams ready
			select {

			case <-:
				return

			// TODO loop over WSs in DiagramsReady. no goroutine per each
			case <-.When1(ss.DiagramsReady, ):
				// msg
				 := WsDiagMsg{
					Type: "refresh",
					Addr: .MachAddr(),
				}
				.Eval("WebSocketDiagState", func() {
					if .params.OutputDiagGroup == types.ParamsOutDiagGroupSkip {
						.Group = .C.SelectedGroup
					}
				}, .Context())
				,  := json.Marshal()
				if  != nil {
					.Log(.Error())
					continue
				}

				// send
				 = .Write(.Context(), websocket.MessageText, )
				if  != nil {
					.EvAddErrState(, ss.ErrWeb, , nil)
					return
				}
			}
		}
	})

	.Fork(, , func() {
		for {
			// msgType, msg, err := ws.Read(r.Context())
			, ,  := .Read(.Context())
			if  != nil {
				.Log("websocket closed")
				if websocket.CloseStatus() == -1 {
					 = fmt.Errorf("websocket read: %w", )
					.EvAddErrState(, ss.ErrWeb, , nil)
				}

				// close up
				close()
				return
			}
		}
	})
}

var _ = ss.FilterCanceledTx

func ( *Debugger) ( *am.Event) {
	// show empty when showing canceled
	.Mach.EvRemove1(, ss.FilterEmptyTx, nil)
}

var _ = ss.FilterQueuedTx

func ( *Debugger) ( *am.Event) {
	// show empty when showing queued
	.Mach.EvRemove1(, ss.FilterEmptyTx, nil)
}

var _ = ss.MatrixRainSelected

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.MatrixRainSelected)
	 := am.ParseArgs[A](.Args)
	 := .Row
	 := .Column
	 := .CurrTxRow
	 := .C
	 := .MsgStruct.StatesIndex
	// _, _, _, height := d.matrix.GetInnerRect()

	// select state name
	if  >= 0 &&  < len() {
		.Mach.EvAdd1(, ss.StateNameSelected, Pass(&A{
			State: [],
		}))
	}

	// scroll to another?
	if  ==  ||  == -1 {
		return
	}

	 :=  - 
	 := .FilterIndexByCursor1(.CursorTx1) + 
	if  == -1 {
		return
	}

	 = .MsgTxsFiltered[] + 1

	// unblock
	.Mach.Fork(, , func() {
		// scroll
		 := amhelp.Add1Sync(, .Mach, ss.ScrollToTx, Pass(&A{
			CursorTx1:   ,
			TrimHistory: true,
		}))
		if .Err() != nil {
			return // expired
		}
		if  {
			return
		}

		// update layout
		.Mach.Eval("MatrixRainSelectedState", func() {
			.hUpdateMatrixRain()
			.updateClientList()
			.hRedrawFull(false)
		}, )
	})
}

var _ = ss.Resized

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Resized)

	.lastResize = .Mach.Time(nil).Sum(nil)
	.hUpdateNarrowLayout()

	// rebuild log
	if .Mach.Not1(ss.ClientSelected) {
		return
	}

	// TODO loose logRebuildEnd and include as relation
	.Mach.EvAdd1(, ss.BuildingLog, Pass(&A{
		LogRebuildEnd: len(.C.MsgTxs),
	}))
	.Mach.Fork(, , func() {
		<-.Mach.When1(ss.LogBuilt, )
		if .Err() != nil {
			return // expired
		}
		// force a redraw TODO bug?
		.draw()
	})
}

var _ = ss.SshServer

func ( *Debugger) ( *am.Event) bool {
	return .params.UiSsh && .params.AddrSsh != ""
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.SshServer)
	// TODO SshClientState
	 := atomic.Bool{}
	 := func( ssh.Session) {
		.Mach.Log("new SSH session " + .RemoteAddr().String())
		if .Load() {
			_, _ = .Write([]byte("am-dbg server busy...\n"))
			_ = .Close()
			return
		}
		// TODO prevent double conns via SshServerConnectedState

		, ,  := .Pty()
		if ! {
			return
		}
		,  := NewSessionScreen()
		if  != nil {
			.Mach.EvAddErr(, , nil)
			return
		}
		.App.SetScreen()
		.Store(true)

		// TODO https://github.com/gliderlabs/ssh/issues/226
		 := make(chan ssh.Signal, 1)
		.Signals()
		defer close()

		// wait till end
		select {
		case <-.Mach.WhenTicks(ss.SshDisconn, 1, nil):
		case <-: // TODO
		case <-.Done():
		}

		// restore sim screen
		.Store(false)
		.App.SetScreen(tcell.NewSimulationScreen("UTF-8"))
	}

	.Mach.Fork(, , func() {
		if .Err() != nil {
			return // expired
		}
		 := func( *ssh.Server) error {
			.sshSrv = 
			return nil
		}

		// show banner TODO optional
		, ,  := net.SplitHostPort(.params.AddrSsh)
		 := .params.Print
		("SSH: listening on %s\n", .params.AddrSsh)
		("\n")
		("Connect via:\n")
		("$ ssh %s -p %s -o UserKnownHostsFile=/dev/null "+
			"-o StrictHostKeyChecking=no\n", .listenHost, )
		.Mach.EvAddErr(
			, ssh.ListenAndServe(.params.AddrSsh, , ), nil,
		)
	})
}

func ( *Debugger) ( *am.Event) {
	.sshSrv.Close()
}