ducko-ui
End-to-end UI testing for the Ducko macOS app using pre-built helper scripts, run in the Lume VM so the user's Mac is not disturbed.
Skill by ducko-im
Chalk Labs
Need help deploying ducko-ui?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/ducko-im-ducko-ui
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: design, testing
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Shell scripts
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/ducko-ui
What's inside
- Skill name
ducko-ui- Description
- End-to-end UI testing for the Ducko macOS app using pre-built helper scripts, run in the Lume VM so the user's Mac is not disturbed. This skill should be used when the user asks to "test ducko", "test the app", "run E2E tests", "test the app end-to-end", "test the UI", "send a test message", or wants to verify DuckoApp works from login through messaging.
- License
- GPL-3.0
- Instructions
- 982 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
Agent Skills spec issues
- body: 982 lines (spec recommends under 500)
- body: contains an absolute path from the author's machine
Files (72)
- SKILL.md
- ducko-account-status.sh · scripts/
- ducko-add-affiliation.sh · scripts/
- ducko-add-bookmark.sh · scripts/
- ducko-add-contact.sh · scripts/
- ducko-attach.sh · scripts/
- ducko-avatar-remove.sh · scripts/
- ducko-avatar.sh · scripts/
- ducko-blocked-contacts.sh · scripts/
- ducko-bookmarks.sh · scripts/
- ducko-change-nickname.sh · scripts/
- ducko-change-password.sh · scripts/
- ducko-channel-search.sh · scripts/
- ducko-chat-header.sh · scripts/
- ducko-chat-tabs.sh · scripts/
- ducko-connect.sh · scripts/
- ducko-connection-info.sh · scripts/
- ducko-contact-info.sh · scripts/
- ducko-contact-menu.sh · scripts/
- ducko-contact-search.sh · scripts/
- ducko-copy-code-block.sh · scripts/
- ducko-copy-code-block.swift · scripts/
- ducko-destroy-room.sh · scripts/
- ducko-device-trust.sh · scripts/
- ducko-dismiss-roster-notice.sh · scripts/
- ducko-dismiss.swift · scripts/
- ducko-edit-profile.sh · scripts/
- ducko-encrypt.sh · scripts/
- ducko-file-panel.swift · scripts/
- ducko-focus-contacts.sh · scripts/
- ducko-helpers.sh · scripts/
- ducko-history-search.sh · scripts/
- ducko-import.sh · scripts/
- ducko-invite-user.sh · scripts/
- ducko-join-room.sh · scripts/
- ducko-launch.sh · scripts/
- ducko-leave-room.sh · scripts/
- ducko-login.sh · scripts/
- ducko-menu-bar.sh · scripts/
- ducko-message-list.sh · scripts/
- ducko-moderate.sh · scripts/
- ducko-new-chat.sh · scripts/
- ducko-preferences-tab.sh · scripts/
- ducko-preferences.sh · scripts/
- ducko-private-message.sh · scripts/
- ducko-profile.sh · scripts/
- ducko-register-fields.sh · scripts/
- ducko-register.sh · scripts/
- ducko-remove-bookmark.sh · scripts/
- ducko-remove-contact.sh · scripts/
- ducko-reply.sh · scripts/
- ducko-retract.sh · scripts/
- ducko-reveal.sh · scripts/
- ducko-room-config-save.sh · scripts/
- ducko-room-settings-tab.sh · scripts/
- ducko-room-settings.sh · scripts/
- ducko-room-topic.sh · scripts/
- ducko-screenshot.sh · scripts/
- ducko-search.sh · scripts/
- ducko-select-mode.sh · scripts/
- …and 12 more
SKILL.md (first lines)
# Ducko UI Test
Run the `/macos-ui-testing` skill first to load generic macOS UI automation patterns. This skill provides Ducko-specific helper scripts on top.
The scripts can be allowlisted in `settings.local.json` (see Permission Allowlisting at the end).
## Window Architecture
DuckoApp ships separate windows:
- **Contact List** (`id: "contacts"`) — singleton, main window after login (roster, status picker, search).
- **Chat** (`id: "chat"`) — singleton tabbed window holding every open conversation as a bottom tab (`chat-tab-bar`). Double-click or New Chat opens or raises it. A roster contact's message or file offer also opens it behind the window the user is in, without taking focus, and adds a tab for that chat without selecting it. A muted chat is left alone. Switching tabs preserves each conversation's draft, search, and sidebar state.
- **Contact Info** (`id: "contact-info"`, keyed by `ContactInfoRef`) — Get Info window: identity, roster/subscription state, vCard, and Block/Remove. Opened from the chat header (i) button, a contact's "Get Info" context item, or Contact ▸ Get Info (⌘⇧I).
- **Chat History** (`id: "transcripts"`) — singleton history window. File ▸ Chat History (⌥⌘T) opens it with the first account selected. The header clock, a contact's "History" context item, and Contact ▸ History (⌘L) retarget it to that conversation. Its title is the name of the conversation whose messages it shows, and "Chat History" while it shows none, so find it by the window's own accessibility identifier, `transcripts`.
- **MenuBarExtra** — quick status, Show Contact List, Quit.
The Chat History window has three parts. A left list holds accounts and imported histories with their conversations, under a "Filter conversations" field that narrows its rows. A day list holds the days of what is selected there, and the message pane shows the selected day. The rows of the two lists carry no identifiers. The toolbar search field searches all conversations and lists the days with matches in the day list. ⌥⌘F (Edit ▸ Search All Conversations) puts the cursor in that field. ⌘F (Edit ▸ Find…) shows and hides a find bar above the messages, which steps through the matches of the listed days.
The Chat window's tabs survive a relaunch, in their order and with the same tab selected. The window itself reopens at launch only when it was open at quit. Otherwise it stays closed, and the tabs come back once a chat is next opened. `ducko-stop.sh` followed by `ducko-launch.sh` is such a relaunch. A test run can therefore start with tabs from an earlier run, and incoming messages can add tabs it did not open. A restored chat window opens in front of Contacts, which leaves the Contacts-only menu commands (Join Room…, Bookmarks…, Add Contact…) disabled, so run `ducko-focus-contacts.sh` after `ducko-launch.sh` before any Contacts step.
`ducko-stop.sh` ends the app without the disconnect a normal quit sends, so the server keeps the old resource for a while and a contact's first messages after the relaunch can go to it and be lost. When a test exchanges messages after a relaunch, send from the relaunched instance first.
A room joined through the Join Room sheet, an accepted invitation or the REPL's `/join` is joined again on every connect until Leave Room, `/leave`, or being kicked or banned. A profile that ended a run inside a room therefore starts the next one in it. To end a run clean, run `ducko-leave-room.sh` for each joined room and `ducko-chat-tabs.sh close-all` before `ducko-stop.sh`.
The Contacts window has **no toolbar**. Its actions live in the app menu bar. File holds New Chat (⌘N), Join Room… (⌘⇧N), Bookmarks… (⌘⇧B) and Close All Chats (⌥⌘W). The Contact menu holds Add Contact… (⌘D) and My Profile…. Its Get Info (⌘⇧I), History (⌘L) and Send File… (⌘⇧F) act on the selected Contacts row or the active chat, while Remove Contact… (⌘⌫) acts only on the selected Contacts row. The Status menu sets presence (⌘⇧Y Available, ⌘Y toggle, Custom…). View holds sort order and Hide Offline (⌘⇧H). Window holds Show/Hide Contact List (⌘/) and chat tab cycling (⌃⇥/⌃⇧⇥). `⌘F` reveals the roster search field (`contact-search-field`). Scripts drive these via keyboard shortcuts or menu-bar clicks (`menu bar item ... of menu bar 1`), not window buttons.
## Prerequisites
The scripts run in the Lume VM (the `/lume-vm` skill) by default. Start it with `zsh Skills/lume-vm/scripts/vm.sh start`, then run each script from the host as `zsh Skills/lume-vm/scripts/vm.sh ui <script> [args]`. Run `vm.sh` outside the Bash sandbox. Every call first copies the working tree into the VM, so edits made mid-session take effect, and it passes `DUCKO_PROFILE` through when set. When `vm.sh` reports that the VM serves another checkout, another session holds it: wait or ask the user, and never fall back to the Mac.
The scripts drive a copy of the debug app named `DuckoUITest`, which `ducko-launch.sh` builds and launches. Without a `DUCKO_PROFILE` the copy runs under the profile `uitest` (`Ducko-Dev-uitest`, `im.ducko.dev.uitest`), so it never shares a store with a debug Ducko on the default profile. Seed that profile's account with the copy's own CLI, after a first `ducko-launch.sh` has built it: `zsh Skills/lume-vm/scripts/vm.sh gui 'DUCKO_PROFILE=uitest /Users/lume/work/ducko/.build/ducko-ui/DuckoUITest.app/Contents/Resources/ducko account add …'`.
Run a script on the Mac only when the user asks for that, with `DUCKO_UI_HOST=1` in its environment. Without it, every script stops with an error before touching any app. On the Mac the scripts still drive only the `DuckoUITest` copy, so an installed Ducko keeps running. The copy does take the foreground, though. A Mac run needs:
- Peekaboo CLI installed (`/opt/homebrew/bin/peekaboo`), for `ducko-reveal.sh`
- Accessibility permissions granted for Terminal/Claude
- An unlocked GUI session. Check with `ioreg -n Root -d1 -a | grep -A1 CGSSessionScreenIsLocked`, run unsandboxed: `<true/>` means locked and no output means unlocked (a sandboxed `ioreg` also prints nothing). While locked, window captures fail and the scripts report windows as not found, so ask the user to unlock rather than polling.
## Scripts
All scripts are in `scripts/` relative to this skill. Run from the repo root or use absolute paths.
Scripts target SwiftUI accessibility identifiers, not positional selectors.
### Accessibility Identifiers
A row whose window is "Menu bar" or "Contacts (menu bar)" names a menu-bar item. Find such an item by its menu and title: menu-bar commands do not expose their identifiers to accessibility.
| Identifier | Element | Window |
|---|---|---|
| `contact-list` | Contact list view | Contacts |
| `contact-row-{jid}` | Individual contact row (account-qualified as `contact-row-{jid}\|{account-jid}` when the JID is on more than one account). Its leading slot shows an unread count (capped at 9+), else a typing bubble while the contact types, else the presence dot. Its accessibility value reads "N unread message(s)" or "Typing", and is empty otherwise. Its label, in the description attribute (the title is unsupported), contains its name and caption, so read it title-then-description, as `AppAccessor.label(identifier:)` does, not from the value. With Hide Offline Contacts on, an offline contact with unread messages keeps its row. | Contacts |
| `status-picker` | Presence status pull-down in the "me" header, listing status rows with colored dots, saved statuses, and Custom…. With 2+ enabled accounts, the status and saved-status rows sit under an "All Accounts" heading, followed by one submenu per account under "Each Account". SwiftUI exposes no identifiers on those submenus or their rows, so address both by title (the account's display name, else its JID). Its value is the header account's message, or its status when no message is set, followed by counts of the accounts showing another status (e.g. `Available · 1 Offline`) | Contacts |
| `custom-status-message-field` | Message field in the Custom Status sheet (status pull-down or Status menu "Custom…" / "Custom Away…") | Contacts |
| `identity-switcher` | Identity (account) switcher menu on the "me" header name, shown with 2+ enabled accounts | Contacts |
| `my-avatar` | Self avatar in the "me" header | Contacts |
| `status-preferences` | Saved-status management list in Preferences ▸ Status | Preferences |
| `saved-status-message-field` | Message field in the Preferences ▸ Status add sheet | Preferences |
| `contact-search-field` | Roster search field, revealed by ⌘F | Contacts |
| `message-field` | Message input text field | Chat |
| `send-button` | Send message button | Chat |
| `new-chat-jid-field` | JID field in New Chat sheet | Chat |
| `new-chat-account-picker` | Account picker in New Chat sheet (shown only when more than one account is enabled) | Chat |
| `start-chat-button` | Start Chat button in sheet | Chat |
| `add-contact-jid-field` | JID field in Add Contact sheet | Contacts |
| `add-contact-error` | Rejected contact-change or invalid-JID error, kept in the sheet. An unconfirmed change closes the sheet and shows `roster-notice` instead | Contacts |
| `add-contact-button` | Add Contact button in sheet | Contacts |
| `rename-contact-field` | Display-name field in the Rename Contact sheet | Contacts |
| `rename-contact-button` | Save button in the Rename Contact sheet | Contacts |
| `jid-field` | JID field in account setup | Contacts |
| `password-field` | Password field in account setup | Contacts |
| `connect-button` | Connect button in account setup | Contacts |
| `reply-compose-bar` | Reply/edit compose bar above input | Chat |
| `message-search-bar` | Cmd+F find bar above the messages. With matches it shows Previous Match and Next Match buttons and a match count. The count reads `2/7`, the current match's number and the total, or the total alone while no match is current | Chat, Chat History |
| `edit-menu-search-all-conversations` | Edit ▸ Search All Conversations (⌥⌘F): puts the cursor in the Chat History window's toolbar search field. Enabled only while that window is focused | Menu bar |
| `sort-mode-menu` | "Sort Contacts" picker, in the View menu bar | Contacts (menu bar) |
| `hide-offline-menu` | "Hide Offline Contacts" toggle (⌘⇧H), in the View menu bar | Contacts (menu bar) |
| `status-menu-{status}` | Status menu row (`available` ⌘⇧Y, `away`, `xa`, `dnd`, `offline`), applied to every enabled account. It carries a checkmark when every enabled account shows that status and a dash when only some do. With 2+ enabled accounts, the rows sit under "All Accounts", followed by the per-account submenus under "Each Account" (addressed by title) | Menu bar |
| `status-menu-toggle` | Status menu ⌘Y item: "Custom Away…" when the header account is Available (opens the Custom Status sheet on Contacts), "Available" for every enabled account otherwise | Menu bar |
| `status-menu-custom` | Status menu "Custom…" item, opening the Custom Status sheet on Contacts | Menu bar |
| `contacts-window-menu` | Window ▸ Hide Contact List / Show Contact List (⌘/): closes the Contacts window when focused, otherwise opens and raises it | Menu bar |
| `next-tab-menu` | Window ▸ Select Next Tab (⌃⇥), wrapping. ⌘⇧] does the same inside the chat window | Menu bar |
| `previous-tab-menu` | Window ▸ Select Previous Tab (⌃⇧⇥), wrapping. ⌘⇧[ does the same inside the chat window | Menu bar |
| `close-all-chats-menu` | File ▸ Close All Chats (⌥⌘W): closes every tab and the chat window | Menu bar |
| `contact-menu-get-info` | Contact ▸ Get Info (⌘⇧I) for the selected Contacts row or the active chat (1:1 contacts only) | Menu bar |
| `contact-menu-history` | Contact ▸ History (⌘L) for the selected Contacts row or the active chat | Menu bar |
| `contact-menu-send-file` | Contact ▸ Send File… (⌘⇧F): opens the chat's file picker (from Contacts, opens the chat first) | Menu bar |
| `contact-menu-remove` | Contact ▸ Remove Contact… (⌘⌫) for the selected contact, with confirmation | Menu bar |
| `room-row-{jid}` | Room row in Rooms section. Its leading slot shows the unread count (capped at 9+), else a green dot when joined or a gray ring when not. Its accessibility value reads "N unread message(s)", and is empty otherwise. Its label contains its name and, when shown, its subject or participant count, read the same way as a contact row's. | Contacts |
| `room-invite-banner` | Pending room invitation banner | Contacts |
| `room-jid-field` | Room JID field in Join Room dialog | Contacts |
| `room-nickname-field` | Nickname field in Join Room dialog | Contacts |
| `join-room-button` | Join button in Join Room dialog | Contacts |
| `browse-rooms-button` | Browse Rooms button in Join Room dialog | Contacts |
| `invite-user-jid-field` | JID field in the Invite User sheet | Contacts |
| `invite-user-reason-field` | Reason field in the Invite User sheet | Contacts |
| `invite-user-button` | Invite button in the Invite User sheet | Contacts |
| `room-subject-view` | Room topic banner (editable) | Chat |
| `participant-sidebar` | Participant sidebar list | Chat |
| `participant-row-{nickname}` | Participant row in the participant sidebar. Right-clicking it opens the participant's context menu. Its nickname text is a separate element inside it. | Chat (sidebar) |
| `toggle-participant-sidebar` | Sidebar toggle button (person.2 icon) | Chat |
| `send-pm-menu-item` | "Send Private Message" context menu item on a MUC participant | Chat (sidebar) |
| `send-directed-presence-menu-item` | "Send Directed Presence" context menu item on a contact | Contacts |
| `attachment-button` | Paperclip file picker button | Chat |
| `pending-attachments` | Pending attachment bar above input | Chat |
| `attachment-send-method` | Pop-up at the end of the pending attachment bar of a 1:1 chat, choosing Upload or Send Directly. Send Directly is greyed out unless the contact is online with a device that takes direct transfers. | Chat |
| `attachment-encryption-note` | "Not end-to-end encrypted" at the end of the pending attachment bar, beneath the pop-up where there is one, while files are queued in a chat with encryption on. Its accessibility label reads "Files are not end-to-end encrypted". | Chat |
| `direct-transfer-status` | Status line under a file sent directly (waiting for the contact to accept, sending, sent directly, or the reason it failed) or under a file being received | Chat |
| `transfer-progress` | Progress strip for a file being uploaded. A file sent directly reports on its own row through `direct-transfer-status`, and one being received on a `receiving-file-row`. | Chat |
| `receiving-file-row` | Row at the end of the message list for a file the chat's contact is sending right now, shown like one of their messages with "Receiving…" under it until the saved file's message replaces it | Chat |
| `file-drop-overlay` | Drag-and-drop overlay | Chat |
| `attachment-view` | Attachment in message bubble | Chat |
| `attachment-preview-button` | File attachment card that opens the system Quick Look panel. Absent for a file not saved on this Mac. | Chat |
| `attachment-reveal-button` | Reveal-in-Finder button on a saved file attachment, shown on hover | Chat |
| `attachment-open-button` | Open button on a remote (HTTP) file attachment | Chat |
| `attachment-load-image` | Click-to-load placeholder for a remote image, showing the file name. Absent where the image loads on sight: your own messages, and a roster contact's messages in a 1:1 chat. Tapping it loads the image inline. | Chat |
| `image-preview` | Full-size image preview sheet | Chat |
| `link-preview` | Link preview card in message bubble | Chat |
| `code-block` | Text of a code block in a message bubble: code that fills whole lines, drawn in its own box. Its "Copy Code" accessibility action copies the code without hovering. | Chat |From ducko-im/ducko, GPL-3.0.
More from ducko-im/ducko
- 0.629score
swift-testing
ducko-im
Expert guidance on Swift Testing for writing, reviewing, migrating, and debugging tests.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
accessibility
ducko-im
Apply platform accessibility best practices to SwiftUI, UIKit, and AppKit code.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swiftui-performance-audit
ducko-im
Audit and improve SwiftUI runtime performance from code review and architecture.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
writing-for-interfaces
ducko-im
Use when someone asks to write, rewrite, review, or improve text that appears inside a product or interface.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
macos-ui-testing
ducko-im
Background-safe macOS app UI testing using Peekaboo CLI and osascript.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
macos-spm-app-packaging
ducko-im
Scaffold, build, and package SwiftPM-based macOS apps without an Xcode project.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
release
ducko-im
Cut a Ducko release: update CHANGELOG.md on main, then push the X.Y.Z tag that triggers the release.yml CI workflow (build, sign, notarize, DMG, Sparkle…
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swift-concurrency
ducko-im
Expert guidance on Swift Concurrency correctness, modern API usage, and async/await pitfalls in Swift 6.2+.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swift-language
ducko-im
Write, review, or improve Swift APIs and value-display formatting.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swift-security
ducko-im
Use when working with iOS/macOS Keychain Services (SecItem queries, kSecClass, OSStatus errors), biometric authentication (LAContext, Face ID, Touch ID)…
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swiftdata
ducko-im
Expert guidance for designing, writing, reviewing, and debugging SwiftData persistence in Swift and SwiftUI apps.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swiftui
ducko-im
Write, review, or improve SwiftUI code across state management, view composition, performance, modern APIs, navigation, animation, accessibility, Liquid Glass…
SkillAFreshness: A★ 7Claude Code +9
Signals
- Stars
- 7
- Forks
- 0
- Open issues
- 4
- Last commit (this folder)
- Oct 23, 2026 (8 days ago)
- Last release
- Oct 23, 2026 (8 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- GPL-3.0
- Repository
- github.com/ducko-im/ducko
- Website
- ducko.im
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from ducko-im
- 0.629score
swift-concurrency
ducko-im
Expert guidance on Swift Concurrency correctness, modern API usage, and async/await pitfalls in Swift 6.2+.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swift-security
ducko-im
Use when working with iOS/macOS Keychain Services (SecItem queries, kSecClass, OSStatus errors), biometric authentication (LAContext, Face ID, Touch ID)…
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
macos-spm-app-packaging
ducko-im
Scaffold, build, and package SwiftPM-based macOS apps without an Xcode project.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
release
ducko-im
Cut a Ducko release: update CHANGELOG.md on main, then push the X.Y.Z tag that triggers the release.yml CI workflow (build, sign, notarize, DMG, Sparkle…
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swift-language
ducko-im
Write, review, or improve Swift APIs and value-display formatting.
SkillAFreshness: A★ 7Claude Code +9 - 0.629score
swiftdata
ducko-im
Expert guidance for designing, writing, reviewing, and debugging SwiftData persistence in Swift and SwiftUI apps.
SkillAFreshness: A★ 7Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get ducko-ui running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
ducko-ui skill. Agent Mart. https://agentmart.dev/skills/ducko-im-ducko-ui (last updated 2026-10-24)