aiui-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ # build artifacts
2
+ companion/dist/
3
+ companion/src-tauri/target/
4
+ companion/src-tauri/gen/schemas/
5
+
6
+ # deps
7
+ companion/node_modules/
8
+
9
+ # packaged release
10
+ aiui-*.zip
11
+ aiui-*.dmg
12
+ aiui-*.tar.gz
13
+ latest.json
14
+
15
+ # macOS
16
+ .DS_Store
17
+
18
+ # logs / local state
19
+ *.log
20
+ /tmp/aiui-trace.log
21
+
22
+ # IDE / editor
23
+ .vscode/
24
+ .idea/
25
+ *.swp
26
+ .aider*
27
+
28
+ # local release credentials
29
+ .env.release
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiui-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server for aiui — native macOS dialogs from any Claude Code session, local or remote.
5
+ Project-URL: Homepage, https://github.com/byte5ai/aiui
6
+ Project-URL: Repository, https://github.com/byte5ai/aiui
7
+ Project-URL: Issues, https://github.com/byte5ai/aiui/issues
8
+ Author-email: byte5 GmbH <cw@byte5.de>
9
+ License-Expression: MIT
10
+ Keywords: claude,dialog,macos,mcp,tauri,ui
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: User Interfaces
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: mcp>=1.26.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # aiui-mcp
26
+
27
+ MCP server for [**aiui**](https://github.com/byte5ai/aiui) — lets Claude Code
28
+ sessions render native macOS dialogs on the user's Mac. Works for local and
29
+ remote Claude Code setups.
30
+
31
+ ## Install
32
+
33
+ Drop this into your project's `.mcp.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "aiui": {
39
+ "command": "uvx",
40
+ "args": ["aiui-mcp"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ `uvx` pulls the latest version automatically. The companion macOS app is
47
+ distributed separately from <https://github.com/byte5ai/aiui/releases>.
48
+
49
+ ## Tools
50
+
51
+ - `aiui.confirm` — hard yes/no with optional destructive styling
52
+ - `aiui.ask` — single- or multi-choice with per-option descriptions
53
+ - `aiui.form` — composite window with typed fields and action buttons
54
+ - `aiui.aiui_health` — reachability check
55
+
56
+ ## Prompts
57
+
58
+ - `/aiui:widgets` — full widget catalog with rules, patterns, anti-patterns
59
+
60
+ ## License
61
+
62
+ MIT — see [LICENSE](https://github.com/byte5ai/aiui/blob/main/LICENSE).
@@ -0,0 +1,38 @@
1
+ # aiui-mcp
2
+
3
+ MCP server for [**aiui**](https://github.com/byte5ai/aiui) — lets Claude Code
4
+ sessions render native macOS dialogs on the user's Mac. Works for local and
5
+ remote Claude Code setups.
6
+
7
+ ## Install
8
+
9
+ Drop this into your project's `.mcp.json`:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "aiui": {
15
+ "command": "uvx",
16
+ "args": ["aiui-mcp"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ `uvx` pulls the latest version automatically. The companion macOS app is
23
+ distributed separately from <https://github.com/byte5ai/aiui/releases>.
24
+
25
+ ## Tools
26
+
27
+ - `aiui.confirm` — hard yes/no with optional destructive styling
28
+ - `aiui.ask` — single- or multi-choice with per-option descriptions
29
+ - `aiui.form` — composite window with typed fields and action buttons
30
+ - `aiui.aiui_health` — reachability check
31
+
32
+ ## Prompts
33
+
34
+ - `/aiui:widgets` — full widget catalog with rules, patterns, anti-patterns
35
+
36
+ ## License
37
+
38
+ MIT — see [LICENSE](https://github.com/byte5ai/aiui/blob/main/LICENSE).
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "aiui-mcp"
3
+ version = "0.2.0"
4
+ description = "MCP server for aiui — native macOS dialogs from any Claude Code session, local or remote."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "byte5 GmbH", email = "cw@byte5.de" },
10
+ ]
11
+ keywords = ["mcp", "claude", "ui", "dialog", "macos", "tauri"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: MacOS :: MacOS X",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: User Interfaces",
22
+ ]
23
+ dependencies = [
24
+ "mcp>=1.26.0",
25
+ "httpx>=0.27",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/byte5ai/aiui"
30
+ Repository = "https://github.com/byte5ai/aiui"
31
+ Issues = "https://github.com/byte5ai/aiui/issues"
32
+
33
+ [project.scripts]
34
+ aiui-mcp = "aiui_mcp.__main__:main"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/aiui_mcp"]
42
+
43
+ [tool.hatch.build.targets.wheel.force-include]
44
+ "src/aiui_mcp/skill.md" = "aiui_mcp/skill.md"
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = [
48
+ "src/aiui_mcp",
49
+ "README.md",
50
+ "pyproject.toml",
51
+ ]
@@ -0,0 +1,4 @@
1
+ """aiui-mcp — MCP server that renders native macOS dialogs via the aiui companion."""
2
+ from .server import main, mcp
3
+
4
+ __all__ = ["main", "mcp"]
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,341 @@
1
+ """aiui MCP server — renders native macOS dialogs via the aiui companion.
2
+
3
+ Topology:
4
+
5
+ Claude Code (local or remote) ──stdio──► aiui-mcp (this process)
6
+ │ HTTP
7
+
8
+ http://127.0.0.1:7777
9
+ │ (local, or via SSH reverse-tunnel)
10
+
11
+ Mac: aiui.app (Tauri companion)
12
+
13
+ The aiui token is read from `~/.config/aiui/token` — installed once when
14
+ the companion runs on the Mac, and scp'd automatically to each remote host
15
+ registered in the companion's settings window.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import importlib.metadata
20
+ import importlib.resources as resources
21
+ import logging
22
+ import os
23
+ import sys
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import httpx
29
+ from mcp.server.fastmcp import FastMCP
30
+
31
+
32
+ def _version() -> str:
33
+ try:
34
+ return importlib.metadata.version("aiui-mcp")
35
+ except importlib.metadata.PackageNotFoundError:
36
+ return "dev"
37
+
38
+
39
+ VERSION = _version()
40
+ BUILD_INFO = f"aiui-mcp v{VERSION}"
41
+
42
+ logging.basicConfig(
43
+ level=os.environ.get("AIUI_LOG_LEVEL", "INFO").upper(),
44
+ format="%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(message)s",
45
+ datefmt="%Y-%m-%d %H:%M:%S",
46
+ stream=sys.stderr,
47
+ )
48
+ log = logging.getLogger("aiui")
49
+ log.info("---- %s started pid=%d ----", BUILD_INFO, os.getpid())
50
+
51
+
52
+ TOKEN_PATH = Path(os.environ.get("AIUI_TOKEN_PATH", "~/.config/aiui/token")).expanduser()
53
+ ENDPOINT = os.environ.get("AIUI_ENDPOINT", "http://127.0.0.1:7777")
54
+ TIMEOUT_S = float(os.environ.get("AIUI_TIMEOUT_S", "120"))
55
+ HEALTH_TIMEOUT_S = float(os.environ.get("AIUI_HEALTH_TIMEOUT_S", "3"))
56
+
57
+ mcp = FastMCP("aiui")
58
+
59
+
60
+ def _token() -> str:
61
+ if not TOKEN_PATH.exists():
62
+ raise RuntimeError(
63
+ f"aiui token not found at {TOKEN_PATH}. "
64
+ "Install the aiui companion on your Mac and register this remote from its "
65
+ "settings window (adds the token automatically). "
66
+ "Download: https://github.com/byte5ai/aiui/releases/latest"
67
+ )
68
+ return TOKEN_PATH.read_text().strip()
69
+
70
+
71
+ async def _preflight() -> None:
72
+ """Quick sanity check before every render call: the service on :7777 must
73
+ accept our bearer token. Guards against stale local aiui instances that
74
+ would otherwise hijack the SSH reverse-forward and hang dialogs silently.
75
+ """
76
+ async with httpx.AsyncClient(timeout=HEALTH_TIMEOUT_S) as client:
77
+ try:
78
+ r = await client.get(
79
+ f"{ENDPOINT}/health",
80
+ headers={"Authorization": f"Bearer {_token()}"},
81
+ )
82
+ except httpx.ConnectError as e:
83
+ raise RuntimeError(
84
+ f"aiui companion not reachable at {ENDPOINT}. "
85
+ f"Is Claude Desktop running on your Mac? For remote projects, the "
86
+ f"SSH reverse-tunnel must also be active (companion handles it "
87
+ f"automatically if this host is registered in its settings). "
88
+ f"Underlying error: {e}"
89
+ ) from e
90
+ except httpx.ReadTimeout as e:
91
+ raise RuntimeError(
92
+ f"aiui companion at {ENDPOINT} timed out on /health — likely a stale "
93
+ f"local aiui instance holding the port. Run `pkill -f '^aiui$'` on "
94
+ f"this host. ({e})"
95
+ ) from e
96
+
97
+ if r.status_code == 401:
98
+ raise RuntimeError(
99
+ f"aiui companion at {ENDPOINT} rejected our token (401). "
100
+ f"Another aiui process may be listening on this port with a different "
101
+ f"token. Run `pkill -f '^aiui$'` on this host, then re-register it "
102
+ f"from the companion's settings window to re-sync the token."
103
+ )
104
+ if r.status_code != 200:
105
+ raise RuntimeError(
106
+ f"aiui companion /health returned {r.status_code}: {r.text[:200]}"
107
+ )
108
+
109
+
110
+ async def _post_render(spec: dict[str, Any]) -> dict[str, Any]:
111
+ await _preflight()
112
+ t0 = datetime.now(timezone.utc)
113
+ log.info("render → kind=%s", spec.get("kind"))
114
+ async with httpx.AsyncClient(timeout=TIMEOUT_S) as client:
115
+ r = await client.post(
116
+ f"{ENDPOINT}/render",
117
+ headers={"Authorization": f"Bearer {_token()}"},
118
+ json={"spec": spec},
119
+ )
120
+ r.raise_for_status()
121
+ dt = (datetime.now(timezone.utc) - t0).total_seconds()
122
+ data = r.json()
123
+ log.info(
124
+ "render ← kind=%s cancelled=%s took=%.2fs",
125
+ spec.get("kind"), data.get("cancelled"), dt,
126
+ )
127
+ return data
128
+
129
+
130
+ def _format_result(payload: dict[str, Any]) -> dict[str, Any]:
131
+ if payload.get("cancelled"):
132
+ return {"cancelled": True}
133
+ return {"cancelled": False, **payload.get("result", {})}
134
+
135
+
136
+ @mcp.tool()
137
+ async def ask(
138
+ question: str,
139
+ options: list[dict[str, str]],
140
+ header: str | None = None,
141
+ multi_select: bool = False,
142
+ allow_other: bool = True,
143
+ ) -> dict[str, Any]:
144
+ """Single- or multi-choice picker with optional free-text fallback.
145
+
146
+ WHEN TO USE: 2–6 mutually-exclusive options where per-option context helps.
147
+ For yes/no, use `confirm`. For mixed inputs, use `form`.
148
+
149
+ WRITE OPTIONS:
150
+ - Label: noun or short imperative, ≤ 5 words, no punctuation, no emoji.
151
+ - Description: one sentence stating the trade-off or consequence.
152
+ - Keep options parallel in grammar.
153
+
154
+ ANTI-PATTERNS: > 8 options (use `form` with a `list` field); generic labels
155
+ like "Option 1"; redundant descriptions that just restate the label.
156
+
157
+ Returns `{cancelled, answers, other?}`. `answers` is a list of values.
158
+
159
+ Args:
160
+ question: Full question, imperative or interrogative.
161
+ options: List of `{"label": str, "description"?: str, "value"?: str}`.
162
+ header: Short chip above the question (≤ 14 chars).
163
+ multi_select: Allow selecting multiple options.
164
+ allow_other: Offer a free-text fallback.
165
+ """
166
+ spec = {
167
+ "kind": "ask",
168
+ "question": question,
169
+ "header": header,
170
+ "options": options,
171
+ "multiSelect": multi_select,
172
+ "allowOther": allow_other,
173
+ }
174
+ return _format_result(await _post_render(spec))
175
+
176
+
177
+ @mcp.tool()
178
+ async def form(
179
+ title: str,
180
+ fields: list[dict[str, Any]],
181
+ description: str | None = None,
182
+ header: str | None = None,
183
+ actions: list[dict[str, Any]] | None = None,
184
+ submit_label: str | None = None,
185
+ cancel_label: str | None = None,
186
+ ) -> dict[str, Any]:
187
+ """Composite window: multiple typed fields + multiple action buttons.
188
+
189
+ WHEN TO USE: ≥ 2 related inputs, or one input plus context/confirmation.
190
+ For yes/no, use `confirm`. For a single choice, use `ask`.
191
+
192
+ WRITE LABELS:
193
+ - Imperative or noun, ≤ 6 words, no punctuation, no emoji.
194
+ - Consistent register across all fields.
195
+ - Field-level descriptions only if the label alone is ambiguous.
196
+
197
+ BE RESTRAINT:
198
+ - ≤ 8 fields per dialog. Split logically if you need more.
199
+ - `static_text` only for context the user couldn't derive from labels.
200
+ - Defaults that a human would actually pick.
201
+
202
+ ACTION BUTTONS:
203
+ - Verb-based and concrete ("Create report"), not "OK".
204
+ - Destructive actions: `destructive: true` — never red a save button.
205
+ - `skip_validation: true` on escape hatches so required-field validation
206
+ doesn't trap the user.
207
+ - ≤ 3 actions.
208
+
209
+ FIELD KINDS:
210
+ - text: {kind, name, label, placeholder?, default?, multiline?, required?}
211
+ - password: {kind, name, label, placeholder?, required?} — masked, treat as secret
212
+ - number: {kind, name, label, default?, min?, max?, step?, required?}
213
+ - select: {kind, name, label, options: [{label, value}], default?, required?}
214
+ - checkbox: {kind, name, label, default?}
215
+ - slider: {kind, name, label, min, max, step?, default?}
216
+ - date: {kind, name, label, default?, required?} — ISO YYYY-MM-DD
217
+ - date_range: {kind, name, label, default?: {from, to}, required?} — result {from, to}
218
+ - color: {kind, name, label, default?} — hex "#RRGGBB"
219
+ - static_text: {kind, text, tone?: "info"|"warn"|"muted"} — display only
220
+ - list: {kind, name, label?, items: [{label, value, description?}],
221
+ selectable?, multi_select?, sortable?, default_selected?: [values]}
222
+ Result: {selected: [values], order: [values]}
223
+ - tree: {kind, name, label?, items: [{label, value, description?, children?: [...]}],
224
+ multi_select?, default_selected?: [values], default_expanded?: [values]}
225
+ Result: {selected: [values]}
226
+
227
+ Returns `{cancelled, action?, values: {name: value, ...}}`.
228
+
229
+ Args:
230
+ title: Window title. Same rules as labels.
231
+ fields: List of field blocks, each with a `kind` from above.
232
+ description: Subtitle, ≤ 2 sentences.
233
+ header: Chip above the title (≤ 14 chars).
234
+ actions: Footer buttons `[{label, value, primary?, destructive?, skip_validation?}]`.
235
+ Without actions, defaults to Cancel + Submit.
236
+ submit_label: Legacy fallback for the default submit button label.
237
+ cancel_label: Legacy fallback for the default cancel button label.
238
+ """
239
+ spec = {
240
+ "kind": "form",
241
+ "title": title,
242
+ "description": description,
243
+ "header": header,
244
+ "fields": fields,
245
+ "actions": actions,
246
+ "submitLabel": submit_label,
247
+ "cancelLabel": cancel_label,
248
+ }
249
+ return _format_result(await _post_render(spec))
250
+
251
+
252
+ @mcp.tool()
253
+ async def confirm(
254
+ title: str,
255
+ message: str | None = None,
256
+ header: str | None = None,
257
+ destructive: bool = False,
258
+ confirm_label: str | None = None,
259
+ cancel_label: str | None = None,
260
+ ) -> dict[str, Any]:
261
+ """Hard yes/no decision with optional destructive styling.
262
+
263
+ WHEN TO USE: irreversible or high-stakes step where "just proceed" is
264
+ unsafe. For pure information, respond in chat. For 3+ options, use `ask`.
265
+
266
+ WRITE:
267
+ - Title: the decision as a question, ≤ 10 words.
268
+ - Message: one sentence stating the concrete consequence.
269
+ - `destructive=True` for deletions/force-pushes/rollbacks — never for
270
+ saves or creates.
271
+ - Custom `confirm_label`/`cancel_label` when verbs clarify.
272
+
273
+ Returns `{cancelled, confirmed}`. `cancelled=True` means Escape or window
274
+ close. `cancelled=False, confirmed=False` means the explicit No button.
275
+
276
+ Args:
277
+ title: The decision phrased as a question.
278
+ message: One-sentence explanation of what happens on confirm.
279
+ header: Chip above the title.
280
+ destructive: Red confirm button.
281
+ confirm_label: Defaults to "Ja".
282
+ cancel_label: Defaults to "Nein".
283
+ """
284
+ spec = {
285
+ "kind": "confirm",
286
+ "title": title,
287
+ "message": message,
288
+ "header": header,
289
+ "destructive": destructive,
290
+ "confirmLabel": confirm_label,
291
+ "cancelLabel": cancel_label,
292
+ }
293
+ return _format_result(await _post_render(spec))
294
+
295
+
296
+ @mcp.prompt()
297
+ def widgets() -> str:
298
+ """The full aiui widget catalog — when to use which dialog, copy
299
+ conventions, anti-patterns, example payloads. Read before composing the
300
+ first dialog in a session."""
301
+ try:
302
+ return (resources.files("aiui_mcp") / "skill.md").read_text()
303
+ except Exception:
304
+ return (
305
+ "aiui skill doc not bundled with this install. "
306
+ "See https://github.com/byte5ai/aiui/blob/main/docs/skill.md"
307
+ )
308
+
309
+
310
+ @mcp.tool()
311
+ async def aiui_health() -> dict[str, Any]:
312
+ """Reachability + token check against the aiui companion.
313
+
314
+ Use this first if dialogs hang or fail — it distinguishes a cold companion
315
+ (user needs to launch Claude Desktop, or the SSH tunnel is down) from a
316
+ rogue local process holding the port with the wrong token.
317
+ """
318
+ try:
319
+ async with httpx.AsyncClient(timeout=HEALTH_TIMEOUT_S) as client:
320
+ r = await client.get(
321
+ f"{ENDPOINT}/health",
322
+ headers={"Authorization": f"Bearer {_token()}"},
323
+ )
324
+ r.raise_for_status()
325
+ data = r.json()
326
+ return {"ok": True, **data, "endpoint": ENDPOINT, "server": BUILD_INFO}
327
+ except Exception as e:
328
+ log.warning("health check failed: %s", e)
329
+ return {"ok": False, "error": str(e), "endpoint": ENDPOINT, "server": BUILD_INFO}
330
+
331
+
332
+ def main() -> None:
333
+ """Entry point for the `aiui-mcp` console script. Default transport is
334
+ stdio (what Claude Code expects). Legacy `--stdio` flag is accepted for
335
+ compatibility with the old script-based invocation."""
336
+ # stdio is the only transport we support; flag-parsing kept minimal.
337
+ mcp.run(transport="stdio")
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: aiui widgets
3
+ description: Render native macOS dialogs on the user's Mac from any Claude Code session — remote or local. Use when the user benefits from structured input (multi-field forms, sortable lists, sliders) more than from chat.
4
+ ---
5
+
6
+ # aiui — Dialog design for Claude agents
7
+
8
+ aiui exposes three MCP tools that render native dialogs on the user's Mac:
9
+
10
+ - `confirm` — irreversible yes/no
11
+ - `ask` — single- or multi-choice with descriptions and optional free-text fallback
12
+ - `form` — composite window with typed fields and multiple action buttons
13
+
14
+ ## When to reach for a dialog vs. chat
15
+
16
+ Prefer chat when the answer fits in one line and the user would type it
17
+ anyway. Prefer a dialog when:
18
+
19
+ - Structured input beats free-form typing (numbers in a range, dates,
20
+ multi-select, ordered lists, secrets).
21
+ - You need several related inputs collected in one step.
22
+ - The decision is destructive or high-stakes and benefits from a clearly-framed
23
+ confirmation.
24
+
25
+ Do **not** use a dialog to display information the chat can render just as
26
+ well (status reports, tables, code snippets, logs).
27
+
28
+ ## Tool choice
29
+
30
+ | Intent | Tool |
31
+ |---|---|
32
+ | Yes/no, especially destructive | `confirm` |
33
+ | 2–6 options, possibly with per-option context | `ask` |
34
+ | Multi-field input, multi-action footer | `form` |
35
+ | Single free-text answer | just ask in chat |
36
+ | More than 8 fields | split into multiple `form` calls; do not cram one dialog |
37
+
38
+ ## Writing labels and copy
39
+
40
+ - Imperative or noun, ≤ 6 words per label, no punctuation, no emoji.
41
+ - Parallel grammar within a dialog. Mix of styles ("Name" / "Bitte geben Sie
42
+ Ihr Alter ein" / "What's your role?") reads as AI slop.
43
+ - Defaults that a real user would pick, not `"enter value here"`.
44
+ - `description`/`static_text` only when the label alone is ambiguous —
45
+ avoid redundancy.
46
+
47
+ ## Action buttons (form only)
48
+
49
+ - Verb-based, concrete. `"Bericht erstellen"` beats `"OK"`.
50
+ - Destructive → `destructive: true`. Never style a save button red.
51
+ - Offer an escape hatch (`skip_validation: true`) so required-field validation
52
+ never traps the user.
53
+ - ≤ 3 actions. If you're tempted to add a fourth, rethink the flow.
54
+
55
+ ## The `list` field — one widget, four modes
56
+
57
+ | `selectable` | `multi_select` | `sortable` | Mode |
58
+ |---|---|---|---|
59
+ | – | – | – | Static info list |
60
+ | ✓ | – | – | Single-choice (radio) |
61
+ | ✓ | ✓ | – | Multi-choice (checkboxes) |
62
+ | – | – | ✓ | Ordering via drag handles |
63
+ | ✓ | ✓ | ✓ | Pick-and-order |
64
+
65
+ Result is always `{selected: [values], order: [values]}` — `order` reflects
66
+ drag changes, `selected` reflects checkbox state.
67
+
68
+ ## Password fields
69
+
70
+ Never collect credentials via `ask` or chat. Use `form` with a `password`
71
+ field so the value stays out of the transcript.
72
+
73
+ ## Anti-patterns (gut vs. schlecht)
74
+
75
+ | Slop | Clean |
76
+ |---|---|
77
+ | `confirm(title="Sicher?")` | `confirm(title="Tabelle 'orders' löschen?", destructive=True, message="18.432 Zeilen werden entfernt.")` |
78
+ | `ask(question="Wählen", options=[{"label": "Option 1"}, …])` | `ask(question="Welche Strategie für die Migration?", options=[{"label":"In-place","description":"Schnell, kein Rollback."}, …])` |
79
+ | `form` mit 15 `text`-Feldern | Aufteilen in logische Schritte oder ganz in Chat verlagern |
80
+ | Button-Labels "OK" / "Abbrechen" | "Deploy starten" / "Verwerfen" |
81
+ | `static_text` echot den Titel | `static_text` ergänzt Kontext, den die Labels nicht transportieren |
82
+
83
+ ## Quick reference example
84
+
85
+ ```python
86
+ aiui.form(
87
+ title="Neuer Feature-Entwurf",
88
+ header="Discovery",
89
+ fields=[
90
+ {"kind": "text", "name": "job", "label": "User-Job",
91
+ "multiline": True, "required": True},
92
+ {"kind": "select", "name": "scope", "label": "Umfang",
93
+ "options": [{"label": "Quick Win", "value": "qw"},
94
+ {"label": "Feature", "value": "f"},
95
+ {"label": "Epic", "value": "e"}],
96
+ "default": "f"},
97
+ {"kind": "list", "name": "stakeholders", "label": "Beteiligte",
98
+ "items": [{"label": "Produkt", "value": "prod"},
99
+ {"label": "Design", "value": "design"},
100
+ {"label": "Engineering", "value": "eng"}],
101
+ "selectable": True, "multi_select": True,
102
+ "default_selected": ["prod", "eng"]},
103
+ {"kind": "date", "name": "deadline", "label": "Zieldatum"},
104
+ ],
105
+ actions=[
106
+ {"label": "Abbrechen", "value": "cancel", "skip_validation": True},
107
+ {"label": "Entwurf speichern", "value": "draft", "skip_validation": True},
108
+ {"label": "Anlegen", "value": "commit", "primary": True},
109
+ ],
110
+ )
111
+ ```
112
+
113
+ Response: `{cancelled: false, action: "commit", values: {job: "...",
114
+ scope: "f", stakeholders: {selected: [...], order: [...]}, deadline: "..."}}`.