Skip to content

feat: Plugin System#268

Closed
eugenioenko wants to merge 97 commits into
mainfrom
feat/widget-system
Closed

feat: Plugin System#268
eugenioenko wants to merge 97 commits into
mainfrom
feat/widget-system

Conversation

@eugenioenko

Copy link
Copy Markdown
Owner

Summary

  • Adds a composable widget system with tree, list, input, button, label, divider, tabs, dialog, drawer, scrollview, select, checkbox, hstack/vstack layout, and box container widgets
  • Replaces legacy hardcoded UI components (explorer, changes panel, info dialog, confirm dialog, input dialog, indent dialog) with composed widget trees
  • Adds focus management system with tab navigation and active border highlighting
  • Adds Surface interface for widget rendering with sub-surface clipping
  • Adds comprehensive widget unit tests (30 → 186 tests)

Context

This is the foundation layer for the upcoming plugin system. The widget system provides reusable, composable UI primitives that both the built-in editor UI and Lua plugins will use.

Test plan

  • All existing tests pass (make test)
  • Widget unit tests cover all widget types
  • Manual testing of replaced UI components (explorer, changes, dialogs)
  • Focus navigation works with Tab/Backtab across all widget containers

🤖 Generated with Claude Code

https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm

eugenioenko and others added 30 commits June 26, 2026 10:10
Implement a reusable tree widget in internal/widgets/ with JSON-serializable
config, multi-action icons, context menus, scrollbar, and expand/collapse.
Register it as a "Widget" sidebar panel with demo Docker data for visual testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Card widget draws a border and insets its child content. Adds DrawBorder
to the widgets.Surface interface and a CardWidgetAdapter bridge. Demo
wraps the tree widget in a card in the Widget sidebar panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Box widget provides configurable padding (top/bottom/left/right) without
borders. Demo updated to use box with padding 1 on all sides.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title widget renders a single-line header with optional dropdown icon
when menu entries are provided. Dropdown widget is a standalone clickable
icon that triggers a menu callback. Demo shows title widget in sidebar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add VStack and HStack layout widgets with grow/fixed sizing based on
each child's Height()/Width() methods. Remainder pixels distributed
across first grow children. Define Widget interface in widgets package.
Add generic WidgetAdapter to replace per-type adapters. Demo shows
Docker view with title + 3 cards in a vstack.

TODO: card border should use theme border style

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Box now handles borders (individual sides), padding, and margin —
replacing Card entirely. Border style defaults to theme StyleBorder.
Corner characters only draw when both adjacent sides are enabled.
Added helper constructors for common cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add BaseWidget with embedded BoxModel (border/padding/margin per side)
  and RenderBox() — all widgets inherit layout via struct embedding
- Remove CardWidget; BoxWidget now delegates to BaseWidget for rendering
- Add JSON widget builder (BuildFromJSON) for hot-reloadable widget panels
- Add "Reload Widgets" command for edit-reload iteration without recompile
- Title and Tree menu icons now use DropdownWidget internally with
  configurable icon and padding via JSON (icon, padded, menuIcon, menuIconPadded)
- Remove per-type adapters; only generic WidgetAdapter remains
- Add widget.json demo file with Docker container/image/volume layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HStack supports left/center/right, VStack supports top/center/bottom.
Alignment applies when all children have fixed sizes. Grow children
fill available space, making alignment irrelevant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ButtonWidget with accelerator key support (underline, not
background highlight), themed styles (StyleButton/StyleButtonFocused
with inverted fg/bg default for focus), and JSON builder wiring.
Replace hardcoded StyleCount with iota sentinel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add FocusManager with flat focusable list, tab/shift+tab cycling,
click-to-focus, and key routing to focused widget only. BoxWidget
shows StyleBorderActive when its child has focus. Button padding
now fills correctly with background color via BorderedInterior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Widget panel was built before ApplyBorderStyle, so it always used
the theme default borders instead of the user's settings override.
Rebuild after border style is applied at startup and at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Label displays plain text (single line, no interactivity).
Divider draws a horizontal rule using the theme border character.
Both wired into the JSON builder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Supports bordered (default, uses theme borders with active highlight)
and borderless (chevron prefix with border color styling) modes. Includes
cursor positioning, double-click word selection, clipboard, and paste.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TabsWidget (standalone tab bar with overflow indicator) and
TabbedWidget (tab container that pairs tabs with child content panels,
switchable from JSON). Fix click-focus bug where mouse forwarding in
FocusManager consumed events before the tab bar could handle them.
Also adds inner padding to bordered inputs and wires tabbed callbacks
on widget panel reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TabsWidget is now focusable with left/right to move a selected cursor
and Space/Enter to activate the tab. Underline indicator shows on the
selected tab label text only (not padding spaces). SetFocused resets
the selection to the active tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add StyleSelectedTab theme style with fallback to SidebarSelected for
backwards compatibility. Reorder focus collection so content widgets
are focused before the tab bar. Highlight overflow chevron when
keyboard-navigating to hidden tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…idget

Close the gaps needed for the tree widget to replace the explorer:
- ActiveID highlights a node independently of selection (open file)
- Muted flag renders node labels in muted style (gitignored/dotfiles)
- OnExpand callback fires on expand for lazy child loading
- Reload preserves expanded state while refreshing via OnExpand
- SelectByID programmatically navigates to a node
- Mouse handler now checks rect bounds before consuming events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allows each node to specify its own badge style (e.g. StyleWarning
for modified, StyleDanger for deleted) instead of always using
StyleMuted. Falls back to StyleMuted when unset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ListItem struct provides a clean flat definition (no children/nesting).
Parsed as "list" type in JSON, maps to TreeWidget internally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a Navigation sidebar panel that reimplements the explorer using
the widget system's TreeWidget. Extracts shared code (LoadDirEntries,
FileOp* helpers) so both explorer and navigation use the same filesystem
loading and file operation logic, keeping them in sync on mutations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generic modal dialog that accepts any widget tree as content and
renders a configurable button row. Supports Tab cycling between
content focusables and buttons, Esc to dismiss, cursor forwarding
for input widgets, and mouse click on buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display widget that takes a string and wraps it at the available
width. Reports Height() from the wrapped line count so layout
containers (VStack, Dialog) can auto-size around it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace custom button rendering, hit regions, and focus tracking with
real ButtonWidget instances in a right-aligned HStack. Adapter's
FocusManager handles Tab cycling. Add OnClick to ButtonConfig, default
button padding 1 left/right, HeightForWidth interface for paragraph
sizing, and demo confirm dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ersed colors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gap property spaces children in layout stacks. Bold field on Cell
enables per-cell bold for styled cells. Dialog and title widgets
now render text in bold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 25 items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete old ui.InputDialogWidget and demo dialog. ShowInputDialog now
composes a DialogWidget with an InputWidget as content. Add
ShowInputDialogEx for custom confirm labels (Open, Review, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete old ui.ConfirmDialogWidget. ShowConfirmDialog now composes a
DialogWidget with a ParagraphWidget as content. Add ShowConfirmDialogEx
for titled confirm dialogs. Fix dialog layout gaps between title,
content, and footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add OnSubmit callback to InputWidget triggered on Enter (not
Shift+Enter). Wire it in ShowInputDialogEx so all input dialogs
submit on Enter. Add "Unsaved Changes" title to quit dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
eugenioenko and others added 27 commits June 26, 2026 11:53
Replace hardcoded 3-column width with computed menuIconWidth()
that accounts for actual icon label and padding config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both padded/non-padded branches computed the same value. Now delegates
to menuIconWidth() which handles both cases correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Iterate runes right-to-left to match the right-to-left layout,
preventing reversed text for multi-character icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Offset rightX by menuIconWidth() so action click detection matches
the rendered position when both actions and menu icon are present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PopupRect() returns absolute screen coords; surface.Sub() treats
args as offsets from origin. Subtract surface origin to prevent
double-offset rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
buildTree uses index-based IDs (pr:0) but handleMenu compared against
name-based IDs (pr:PR #42) which never matched. Use label match only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove 23 step-by-step slog calls that were left from debugging
the diff opening flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Expanded, StagedExpanded, ChangesExpanded, and Input fields
that were never read.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove type assertions to *RenderSurface for Origin() calls.
All Surface implementations now provide Origin() directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warn users that building from source uses the latest development
code which may be less stable than official releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent blank rows at bottom when tree shrinks (collapse, delete,
reload) by clamping scrollTop to max valid position in Render.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TestRootGlobalKeysFire now uses passThroughWidget (returns EventIgnored)
so global keys still fire. Added TestRootFocusedWidgetBlocksGlobalKey
to verify that a consuming widget prevents global key dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the core plugin infrastructure: manifest loading, permission
model with diff/enforcement, sandboxed gopher-lua VMs, and the full
render pipeline from Lua to sidebar panel via Surface bridging.

Plugins live in ~/.config/ttt/plugins/<name>/ with a plugin.ttt.json
manifest. On startup, permissions are diffed against the registry and
unapproved plugins trigger a KeyValueList approval dialog. Approved
plugins can register sidebar tabs with raw cell rendering (panel:text,
panel:cell, panel:clear, panel:size).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the plugin system with a declarative widget API (label, tree, list,
button, input) backed by a reconciliation engine that preserves interactive
state across re-renders. Add bottom panel registration, event routing with
focus management, and plugin authoring documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… (Phase 3)

Implement the full Lua API surface for plugins to interact with the
editor and external systems. Each module is gated by permissions and
accessed via require("ttt.editor"), require("ttt.fs"), etc.

- ttt.editor: read/write buffer, cursor, selection, file path
- ttt.fs: read, write, exists, list files
- ttt.system: exec (with binary allowlist), exec_async, env
- ttt.net: HTTP get/post with sync and async variants
- ttt.events: file.open, file.close, file.save, editor.change listeners
- Async operations use goroutine + PostEvent pattern for non-blocking UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pdate, and plugins panel (Phase 4)

Plugins can now register commands and keybindings from Lua via
ttt.register(), gated by permissions. Full lifecycle management:
install from git URL, uninstall with cleanup, update with permission
diff detection, and enable/disable toggle.

Adds a dedicated Plugins sidebar panel with installed/available
sections, backed by async remote registry fetch. Install, update,
and uninstall operations run asynchronously with PostEvent delivery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… consolidation (Phase 5)

Add OUTPUT bottom panel for plugin logging, ttt.log() Lua API for plugins
to write messages, error routing to OUTPUT, plugin.reload/reloadAll commands,
and consolidate ProblemsWidget/OutputWidget onto a shared ListWidget with
RenderItem callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… plugins dir

- Add vstack, box, dropdown panel widget types for Lua plugins
- Fix widget reconciliation: box children are now reused across renders
  instead of recreated, preserving tree selection state
- Fix FocusManager: return EventIgnored with 0 items, route mouse by
  position, preserve focused widget across Collect() calls
- Add context menu support for tree/list node_menu and on_command
- Wire Borders and ShowContextMenu to plugins for theme-consistent UI
- Add HasPanel guard to prevent duplicate panel additions on restart
- Load plugins from workspace-local plugins/ dir in addition to global
- Add docker-manager example plugin
- Add E2E test for plugin panel event routing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
- Plugins can register sidebar actions via sidebar.actions and on_action
  callback, shown in the ⋮ header menu when the plugin panel is active
- Add ttt.show_info(title, entries) Lua API for key-value help dialogs
- Move Docker plugin dropdown into sidebar actions menu with Help entry
- Wire ShowInfoDialog callback from plugin to app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
…uIconPadded

- Only reserve scrollbar column when scrollbar is visible, fixing
  extra blank space on the right when content fits without scrolling
- Remove MenuIconPadded wiring from plugin API (unpadded default
  already provides a 1-column gap)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
- Add p:title() widget (bold section heading) to Lua plugin API
- Add p:keyvalue() widget (key-value list) to Lua plugin API
- Add badge field to label widget for right-aligned secondary text
- Add box model support (margin/padding) via parseBoxModel helper
- Document Plugin Widget API in CLAUDE.md
- Use label with badge for Docker plugin section headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
…click fix

- Add ttt.confirm(message, callback) Lua API for confirmation dialogs
- Wrap all destructive Docker plugin actions with confirmation dialogs
- Add per-side border support (border_top/bottom/left/right) on box widget
- Add keyvalue widget (p:keyvalue) to Lua plugin API
- Add badge field to label widget for right-aligned secondary text
- Fix tree menu/action click zones to use contentW instead of rect.W
- Use label with badge for Docker plugin section headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
…ugin docs update

- Add ttt.open_drawer/close_drawer Lua API for slide-out panels
- Add ttt.open_tab/close_tab Lua API for editor group tabs
- Add panel.editor permission for plugin editor tabs
- Wire OpenPluginTab/ClosePluginTab on EditorGroupWidget
- Comprehensive update to docs/PLUGINS.md with all widget and API docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
…ents

Fix tree node expansion bug where root expanded state was restored after
flatten(), making children invisible. Fix box widget ignoring padding/margin
from Lua. Add key_commands support to tree/list widgets for single-key
shortcuts that fire on_command with the selected node. Update go-test-runner
to use on_command instead of on_select, fix UTF-8 icons, add padding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
5 parallel review agents audited the plugin system for bugs, security,
architecture, code duplication, and API consistency. Consolidated into
a prioritized checklist (6 critical, 10 high, 55 medium, 19 low).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GG9dYw2AG18ro5c2dkPtm
@eugenioenko eugenioenko changed the title feat: composable widget system feat: Plugin System Jun 27, 2026
@eugenioenko eugenioenko deleted the feat/widget-system branch June 27, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant