Skip to content

Commit 9f11ed0

Browse files
authored
Merge pull request #38 from engalar/feat/tui-agent
feat(tui): add agent channel for external automation
2 parents 37b4438 + de6fa76 commit 9f11ed0

64 files changed

Lines changed: 12118 additions & 8035 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
---
2+
name: tui-agent-channel
3+
description: Use when Claude needs to send MDL commands to a running mxcli TUI, execute operations with human oversight, or automate TUI interactions via the agent socket. Also use when setting up Claude-TUI integration for supervised Mendix project modifications.
4+
---
5+
6+
# TUI Agent Channel
7+
8+
Send MDL commands to a running mxcli TUI over a Unix socket. **All agent actions simulate normal user operations** — every command is visible in the TUI, understandable by the human, and interruptible.
9+
10+
## Core Principle
11+
12+
**AI automation = simulated user operations.** No special privileges, no bypassing the UI. Agent actions route through the same views (ExecView, ConfirmView, InputView) that a human uses.
13+
14+
| Agent action | Maps to human action |
15+
|-------------|---------------------|
16+
| `exec` | Human presses `x` → ExecView with MDL → Ctrl+E to execute |
17+
| `list` | Same as `exec` with `SHOW ...` command |
18+
| `describe` | Same as `exec` with `DESCRIBE ...` command |
19+
| `delete` | Human presses `D` → ConfirmView → `y` to confirm |
20+
| `create_module` | Human presses `C` → InputView → Enter to submit |
21+
| `navigate` | Human presses `Space` → jumps to element |
22+
| `state` | Read-only query (no UI side effect) |
23+
| `format` | Pure text computation (no project interaction) |
24+
25+
## Setup
26+
27+
### 1. Start TUI with agent socket
28+
29+
```bash
30+
# Human confirmation mode (default) — user must press Ctrl+E / y / Enter / q for each operation
31+
mxcli tui -p app.mpr --agent-socket /tmp/mxcli-agent.sock
32+
33+
# Auto-proceed mode — views auto-execute but remain visible for review
34+
mxcli tui -p app.mpr --agent-socket /tmp/mxcli-agent.sock --agent-auto-proceed
35+
```
36+
37+
### 2. Send commands via socat
38+
39+
```bash
40+
# All UI-visible actions need socat -t timeout (they go through views)
41+
printf '{"id":1,"action":"exec","mdl":"SHOW ENTITIES"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
42+
43+
# check uses Docker mx check — allow longer timeout
44+
printf '{"id":2,"action":"check"}\n' | socat -t 120 - UNIX-CONNECT:/tmp/mxcli-agent.sock
45+
46+
# state and format are instant (no UI round-trip)
47+
echo '{"id":3,"action":"state"}' | socat - UNIX-CONNECT:/tmp/mxcli-agent.sock
48+
```
49+
50+
## Protocol
51+
52+
JSON-line protocol over Unix socket. One request per line, one JSON response per line.
53+
54+
### Request
55+
56+
```json
57+
{"id": 1, "action": "exec", "mdl": "CREATE ENTITY Mod.E (Name: String(100));"}
58+
```
59+
60+
| Field | Required by | Description |
61+
|-------|-------------|-------------|
62+
| `id` | all | Unique request ID (nonzero integer) |
63+
| `action` | all | Action name (see Actions table) |
64+
| `mdl` | `exec`, `format` | MDL statement(s) or text |
65+
| `target` | `navigate`, `delete`, `describe`, `list` | Element reference (see Target Format) |
66+
| `name` | `create_module` | Module name to create |
67+
68+
### Target Format
69+
70+
Targets use `type:qualifiedName` format:
71+
72+
| Example | Meaning |
73+
|---------|---------|
74+
| `entity:Module.Entity` | A specific entity |
75+
| `microflow:Module.MF_Name` | A specific microflow |
76+
| `module:MyModule` | A module |
77+
| `entities` | All entities (for `list`) |
78+
| `entities:MyModule` | Entities in a module (for `list`) |
79+
80+
Supported types for `delete`/`describe`: `entity`, `association`, `enumeration`, `constant`, `microflow`, `page`, `snippet`, `workflow`, `imagecollection`, `javaaction`, `module`
81+
82+
Supported types for `list`: plural forms (`entities`, `microflows`, `pages`, `modules`, etc.)
83+
84+
### Response
85+
86+
```json
87+
{"id": 1, "ok": true, "result": "Created entity: Mod.E", "mode": "overlay:exec-result", "changes": [{"action":"created","target":"entity: Mod.E"}]}
88+
```
89+
90+
| Field | Description |
91+
|-------|-------------|
92+
| `id` | Echoed request ID |
93+
| `ok` | `true` if operation succeeded |
94+
| `result` | Output text (same as TUI overlay content) |
95+
| `error` | Error message (when `ok` is `false`) |
96+
| `mode` | TUI state after operation |
97+
| `changes` | Structured changes array (write operations only, when applicable) |
98+
99+
### State Response
100+
101+
The `state` action returns a structured JSON object:
102+
103+
```json
104+
{
105+
"mode": "Browse",
106+
"project": "/path/to/app.mpr",
107+
"selectedNode": {"type": "entity", "qualifiedName": "MyModule.Customer"},
108+
"previewMode": "MDL",
109+
"checkErrors": 0,
110+
"checkRunning": false
111+
}
112+
```
113+
114+
## Actions
115+
116+
| Action | UI View | Visible | Example |
117+
|--------|---------|---------|---------|
118+
| `exec` | ExecView → Overlay | yes | `{"id":1,"action":"exec","mdl":"CREATE ENTITY M.E (X: String(100));"}` |
119+
| `list` | ExecView → Overlay | yes | `{"id":2,"action":"list","target":"entities:MyModule"}` |
120+
| `describe` | ExecView → Overlay | yes | `{"id":3,"action":"describe","target":"entity:M.E"}` |
121+
| `check` | Status bar → Overlay | yes | `{"id":4,"action":"check"}` |
122+
| `delete` | ConfirmView → Overlay | yes | `{"id":5,"action":"delete","target":"entity:M.E"}` |
123+
| `create_module` | InputView → Overlay | yes | `{"id":6,"action":"create_module","name":"NewModule"}` |
124+
| `navigate` | Browser (miller columns) | yes | `{"id":7,"action":"navigate","target":"entity:M.E"}` |
125+
| `state` || no | `{"id":8,"action":"state"}` |
126+
| `format` || no | `{"id":9,"action":"format","mdl":"create entity m.e(x:string(100));"}` |
127+
128+
## Human Confirmation Flow
129+
130+
### Without `--agent-auto-proceed` (default)
131+
132+
1. Agent sends command via socket
133+
2. TUI pushes the appropriate view (ExecView/ConfirmView/InputView)
134+
3. **Human must act**: press Ctrl+E to execute, `y` to confirm delete, Enter to submit, or Esc to cancel
135+
4. Result displayed in overlay — human presses `q`/`Esc` to dismiss
136+
5. Response sent back to agent
137+
138+
**Cancellation**: If human presses Esc in any view, agent receives `{"ok":false,"error":"cancelled by user"}`.
139+
140+
### With `--agent-auto-proceed`
141+
142+
1. Agent sends command via socket
143+
2. TUI pushes the view and **auto-triggers** the action (Ctrl+E / y / Enter)
144+
3. Result displayed in overlay — response sent immediately to agent
145+
4. Human can still review the overlay and press `q`/`Esc` to dismiss at leisure
146+
147+
The status bar shows `⚡agent` badge while an agent operation is in progress.
148+
149+
## Typical Claude Workflow
150+
151+
```bash
152+
# 1. Check current state
153+
echo '{"id":1,"action":"state"}' | socat - UNIX-CONNECT:/tmp/mxcli-agent.sock
154+
155+
# 2. List entities in a module (human sees ExecView + Overlay)
156+
printf '{"id":2,"action":"list","target":"entities:MyModule"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
157+
158+
# 3. Describe an entity (human sees ExecView + Overlay)
159+
printf '{"id":3,"action":"describe","target":"entity:MyModule.Customer"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
160+
161+
# 4. Create a new module (human sees InputView + Overlay)
162+
printf '{"id":4,"action":"create_module","name":"Orders"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
163+
164+
# 5. Execute MDL (human sees ExecView + Overlay)
165+
printf '{"id":5,"action":"exec","mdl":"CREATE ENTITY Orders.Order (OrderNo: String(20), Total: Decimal);"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
166+
167+
# 6. Delete an entity (human sees ConfirmView + Overlay)
168+
printf '{"id":6,"action":"delete","target":"entity:Orders.OldEntity"}\n' | socat -t 30 - UNIX-CONNECT:/tmp/mxcli-agent.sock
169+
170+
# 7. Format MDL text (instant, no UI)
171+
echo '{"id":7,"action":"format","mdl":"create entity m.e(x:string(100));"}' | socat - UNIX-CONNECT:/tmp/mxcli-agent.sock
172+
173+
# 8. Navigate to see result (human sees browser jump)
174+
echo '{"id":8,"action":"navigate","target":"entity:Orders.Order"}' | socat - UNIX-CONNECT:/tmp/mxcli-agent.sock
175+
176+
# 9. Verify with check (human sees status bar + Overlay)
177+
printf '{"id":9,"action":"check"}\n' | socat -t 120 - UNIX-CONNECT:/tmp/mxcli-agent.sock
178+
```
179+
180+
## Common Mistakes
181+
182+
| Mistake | Fix |
183+
|---------|-----|
184+
| Missing newline after JSON | Append `\n` — protocol is line-delimited |
185+
| `id: 0` | ID must be nonzero |
186+
| `exec` without `mdl` field | Always include MDL text for exec action |
187+
| Socket not found | Ensure TUI is running with `--agent-socket` |
188+
| Response never arrives | In confirmation mode, human must act in the view (Ctrl+E / y / Enter / q) |
189+
| `echo` pipe closes before response | Use `printf '...\n' \| socat -t 30` for all UI-visible actions |
190+
| Overlay stays open after auto-proceed | By design — human can still review; press `q`/`Esc` to dismiss |
191+
| Sending navigate while overlay is open | Close overlay first (`Esc`/`q`) — navigate only works in Browse mode |
192+
| `delete` target missing colon | Use `entity:Module.Entity` format, not just `Module.Entity` |
193+
| `list` using singular type | Use plural: `entities` not `entity`, `microflows` not `microflow` |
194+
| `list`/`describe` timeout | These now go through ExecView — use `socat -t 30`, not bare `echo` |
195+
196+
## Architecture Notes
197+
198+
**No special privileges**: All agent actions (except `state` and `format`) go through the same bubbletea views that humans use. `list` and `describe` are converted to `SHOW`/`DESCRIBE` MDL commands and routed through `AgentExecMsg` → ExecView. `delete` goes through ConfirmView. `create_module` goes through InputView.
199+
200+
**agentExecContext**: Tracks agent-initiated UI operations. When an agent action pushes a view (ExecView/ConfirmView/InputView), the response channel is stored in `agentExecContext`. When the view completes (`execShowResultMsg`), the response is sent back. If the user cancels (`PopViewMsg` with Esc), a rejection response is sent.
201+
202+
**bubbletea model copy**: bubbletea copies the model at `tea.NewProgram()` time. The `agentAutoProceed` flag is set via `SetAgentAutoProceed()` before `NewProgram`. The `agentListener` (set after) is only used for lifecycle management.
203+
204+
**check action uses Docker `mx check`**: Triggers `runMxCheck()` which uses Docker-based `mx check` (the Mendix project checker), not `mxcli check` (MDL syntax checker). Result returned via `MxCheckResultMsg` and forwarded through `agentCheckCh`.
205+
206+
**Status bar badge**: `⚡agent` badge shown in the status bar while `agentExecCtx` is non-nil (agent operation in progress).
207+
208+
**Structured changes**: Write operations (`exec`, `delete`, `create_module`) include a `changes` array extracted by regex-matching output lines like "Created entity: Mod.E".
209+
210+
## Key Files
211+
212+
- `cmd/mxcli/tui/agent_protocol.go` — Request/Response types, `parseTarget()`, `buildListCmd()`, `buildAgentDescribeCmd()`
213+
- `cmd/mxcli/tui/agent_msgs.go` — tea.Msg types for agent actions
214+
- `cmd/mxcli/tui/agent_listener.go` — Unix socket server, sync handler (format only), async dispatch for all other actions
215+
- `cmd/mxcli/tui/app.go` — Update handlers, `agentExecContext`, `agentBuildState()`, `agentParseChanges()`
216+
- `cmd/mxcli/tui/execview.go` — ExecView (used by exec/list/describe)
217+
- `cmd/mxcli/tui/confirmview.go` — ConfirmView (used by delete), `buildDropCmd()`
218+
- `cmd/mxcli/tui/inputview.go` — InputView (used by create_module)
219+
- `cmd/mxcli/cmd_tui.go` — CLI flags (`--agent-socket`, `--agent-auto-proceed`)

cmd/mxcli/cmd_bson_dump.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Object Types:
3030
enumeration Dump an enumeration
3131
snippet Dump a snippet
3232
layout Dump a layout
33+
constant Dump a constant
3334
3435
Examples:
3536
# List all pages
@@ -337,7 +338,7 @@ func formatValue(val any) string {
337338
}
338339

339340
func init() {
340-
bsonDumpCmd.Flags().StringP("type", "t", "page", "Object type: page, microflow, nanoflow, enumeration, snippet, layout")
341+
bsonDumpCmd.Flags().StringP("type", "t", "page", "Object type: page, microflow, nanoflow, enumeration, snippet, layout, constant")
341342
bsonDumpCmd.Flags().StringP("object", "o", "", "Object qualified name to dump (e.g., Module.PageName)")
342343
bsonDumpCmd.Flags().BoolP("list", "l", false, "List all objects of the specified type")
343344
bsonDumpCmd.Flags().StringSliceP("compare", "c", nil, "Compare two objects: --compare Obj1,Obj2")

cmd/mxcli/cmd_fmt.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"os"
8+
9+
"github.com/mendixlabs/mxcli/mdl/formatter"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var fmtCmd = &cobra.Command{
14+
Use: "fmt <file.mdl>",
15+
Short: "Format an MDL file",
16+
Long: `Format an MDL script file with consistent styling:
17+
- Uppercase MDL keywords
18+
- Normalize indentation (2-space units)
19+
- Remove trailing whitespace
20+
- Normalize blank lines
21+
22+
Examples:
23+
# Format to stdout
24+
mxcli fmt script.mdl
25+
26+
# Format in-place
27+
mxcli fmt script.mdl -w
28+
`,
29+
Args: cobra.ExactArgs(1),
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
filePath := args[0]
32+
writeInPlace, _ := cmd.Flags().GetBool("write")
33+
34+
data, err := os.ReadFile(filePath)
35+
if err != nil {
36+
return fmt.Errorf("failed to read file: %w", err)
37+
}
38+
39+
formatted := formatter.Format(string(data))
40+
41+
if writeInPlace {
42+
if err := os.WriteFile(filePath, []byte(formatted), 0644); err != nil {
43+
return fmt.Errorf("failed to write file: %w", err)
44+
}
45+
fmt.Fprintf(os.Stderr, "Formatted %s\n", filePath)
46+
} else {
47+
fmt.Print(formatted)
48+
}
49+
50+
return nil
51+
},
52+
}
53+
54+
func init() {
55+
fmtCmd.Flags().BoolP("write", "w", false, "Write result to source file instead of stdout")
56+
}

cmd/mxcli/cmd_tui.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,24 @@ Example:
8787
if session != nil {
8888
m.SetPendingSession(session)
8989
}
90+
91+
// Set agent auto-proceed BEFORE tea.NewProgram so the model copy has the value
92+
agentSocket, _ := cmd.Flags().GetString("agent-socket")
93+
agentAutoProceed, _ := cmd.Flags().GetBool("agent-auto-proceed")
94+
if agentSocket != "" {
95+
m.SetAgentAutoProceed(agentAutoProceed)
96+
}
97+
9098
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
9199
m.StartWatcher(p)
100+
101+
if agentSocket != "" {
102+
if err := m.StartAgentListener(p, agentSocket, agentAutoProceed); err != nil {
103+
fmt.Fprintf(os.Stderr, "Warning: agent listener failed: %v\n", err)
104+
}
105+
defer m.CloseAgentListener()
106+
}
107+
92108
if _, err := p.Run(); err != nil {
93109
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
94110
os.Exit(1)
@@ -98,4 +114,6 @@ Example:
98114

99115
func init() {
100116
tuiCmd.Flags().BoolP("continue", "c", false, "Restore previous TUI session")
117+
tuiCmd.Flags().String("agent-socket", "", "Unix socket path for agent communication (e.g. /tmp/mxcli-agent.sock)")
118+
tuiCmd.Flags().Bool("agent-auto-proceed", false, "Skip human confirmation for agent operations")
101119
}

cmd/mxcli/lsp_completions_gen.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/mxcli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,5 @@ func init() {
304304
rootCmd.AddCommand(playwrightCmd)
305305
rootCmd.AddCommand(evalCmd)
306306
rootCmd.AddCommand(tuiCmd)
307+
rootCmd.AddCommand(fmtCmd)
307308
}

0 commit comments

Comments
 (0)