ratatui_ruby-tea 0.3.0 → 0.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d10ee51cb79e58e9b24dbf78e696dcbc901ddedac44dce824c4cf8817e8b07a3
4
- data.tar.gz: 1043f33f9e3804748ea3fdd5eef4783b7af3a81ed51b10ce88f3447953e1874e
3
+ metadata.gz: c83e509aceb1826713abd9e4835055c1d28397627438d3041783564195b1821b
4
+ data.tar.gz: d452ab612362d0d3216101ab9d454c950a8af07afcd913c3fa8627e69eb09887
5
5
  SHA512:
6
- metadata.gz: e25205b29e1654964c88455f9a9fed89ee3464aacbecc458e128baae8bf0a788196e9c1fbdee9cea3549b0e9f60cf1bef5c48c870baa57af453759ad4625453b
7
- data.tar.gz: 121c207a7c82109eafbcdbe7ebae838cccb33f54a0f6ff96c75832294e07496284c26c89fdf7bf0f871883959951414ab52582d209c52b1fb00249a0b009ab9f
6
+ metadata.gz: 6c211329907d95912192da187769d97ef56142cb4b1abe87ed2ed38c61b519d31f6823ab7f1154bc930e430b5fb19f1301c9497a9c68856c3c136c1ec0f6b962
7
+ data.tar.gz: 12b22830443dcc0783323bcccb8f2c1337a416cfe0cebaf89b0988d949f68e3ab73c4fbc37a6c140bc8b103465c8e467dad443654259961987dd91de7fa95767
data/AGENTS.md CHANGED
@@ -55,10 +55,14 @@ Description: Part of the RatatuiRuby ecosystem.
55
55
 
56
56
  ## 3. Definition of Done (DoD)
57
57
 
58
- Before considering a task complete:
58
+ Before considering a task complete and returning control to the user, you **MUST** ensure:
59
59
 
60
60
  0. **Production Ready:** RBS types are complete and accurate (no `untyped`), errors are handled with good DX, documentation follows guidelines, high code quality (no "pre-existing debt" excuses).
61
- 1. **Default Rake Task Passes:** Run `bundle exec agent_rake` (no args). Confirm it passes with ZERO errors.
62
- 2. **Documentation Updated:** If public APIs changed, update relevant docs.
63
- 3. **Changelog Updated:** If public APIs changed, update CHANGELOG.md's **Unreleased** section.
64
- 4. **Commit Message Suggested:** Include a suggested commit message block.
61
+ 1. **Default Rake Task Passes:** Run `bundle exec agent_rake` (no args). Confirm it passes with ZERO errors **or warnings**.
62
+ - You will save time if you run `bundle exec agent_rake rubocop:autocorrect` first.
63
+ - If you think the rake is looking for deleted files, STOP EVERYTHING and tell the user.
64
+ 2. **Documentation Updated:** If public APIs or observable behavior changed, update relevant RDoc, rustdoc, `doc/` files, `README.md`, and/or `ratatui_ruby-wiki` files.
65
+ 3. **Changelog Updated:** If public APIs, observable behavior, or gemspec dependencies have changed, update [CHANGELOG.md](CHANGELOG.md)'s **Unreleased** section.
66
+ 4. **Commit Message Suggested:** You **MUST** ensure the final message to the user includes a suggested commit message block. This is NOT optional.
67
+ - You MUST also check `git log -n1` to see the current standard AI footer ("Generated with" and "Co-Authored-By") and include it in your suggested message.
68
+
data/CHANGELOG.md CHANGED
@@ -21,6 +21,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
21
 
22
22
  ### Removed
23
23
 
24
+ ## [0.3.1] - 2026-01-11
25
+
26
+ ### Added
27
+
28
+ - **CancellationToken**: Cooperative cancellation mechanism for long-running custom commands. Commands check `cancelled?` periodically and stop gracefully when `cancel!` is called. Includes `CancellationToken::NONE` null object for commands that ignore cancellation.
29
+
30
+ - **Command::Custom Mixin**: Include in your class to mark it as a custom command. Provides `tea_command?` brand predicate and `tea_cancellation_grace_period` (default 2.0 seconds) for configuring cleanup time after cancellation.
31
+
32
+ - **Command::Outlet**: Messaging gateway for custom commands. Use `put(tag, *payload)` to send results back to the update function. Validates Ractor-shareability in debug mode.
33
+
34
+ - **Custom Command Dispatch**: Runtime now dispatches custom commands (objects with `tea_command?` returning true) in background threads. Commands receive an `Outlet` for messaging and a `CancellationToken` for cooperative shutdown.
35
+
36
+ - **Command.custom Factory**: Wraps lambdas/procs to give them unique identity for dispatch tracking. Each `Command.custom(callable)` call produces a distinct wrapper, enabling targeted cancellation. Accepts optional `grace_period:` to override the default 2.0 second cleanup window.
37
+
38
+ - **Command.cancel Factory**: Request cancellation of a running command. Returns a `Command::Cancel` sentinel that the runtime routes to the appropriate command's CancellationToken.
39
+
40
+ - **Runtime Cancellation Dispatch**: The runtime now handles `Command::Cancel` by signaling the target command's `CancellationToken`, enabling cooperative cancellation of long-running commands. Respects `tea_cancellation_grace_period`: waits for the grace period, then force-kills unresponsive threads. Use `Float::INFINITY` to never force-kill.
41
+
42
+ - **Graceful Shutdown**: On exit, runtime signals all active commands then respects each command's grace period. Commands with `Float::INFINITY` grace are waited on indefinitely (user has SIGKILL). Final queue messages are processed before returning.
43
+
44
+ - **Automatic Error Propagation**: Custom commands that raise unhandled exceptions now produce a `Command::Error` message instead of corrupting the TUI display. The runtime catches exceptions and pushes `Command::Error.new(command:, exception:)` to the queue. Pattern match on `Command::Error` in your update function to handle failures uniformly. Factory method `Command.error(command, exception)` is available for testing.
45
+
46
+ - **Command::System Cancellation**: Streaming shell commands now respect cooperative cancellation. When cancelled, sends `SIGTERM` for graceful shutdown, then `SIGKILL` if the child process doesn't exit. Prevents orphaned child processes from lingering after app exit.
47
+
48
+ ### Changed
49
+
50
+ ### Fixed
51
+
52
+ - **Ractor Enforcement is Debug-Only**: The Ractor-shareability check now only runs in debug mode (and automated tests). Production skips this check for performance, matching the original specification. Previously, the check ran unconditionally.
53
+
54
+ ### Removed
55
+
24
56
  ## [0.3.0] - 2026-01-08
25
57
 
26
58
  ### Added
@@ -83,6 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
83
115
  - **First Release**: Empty release of `ratatui_ruby-tea`, a Ruby implementation of The Elm Architecture (TEA) for `ratatui_ruby`. Scaffolding generated by `ratatui_ruby-devtools`.
84
116
 
85
117
  [Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/HEAD
118
+ [0.3.1]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.3.1
86
119
  [0.3.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.3.0
87
120
  [0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
88
121
  [0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
data/README.md CHANGED
@@ -26,6 +26,23 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
26
26
 
27
27
  Please join the **announce** mailing list at https://lists.sr.ht/~kerrick/ratatui_ruby-announce to stay up-to-date on new releases and announcements. See the [`trunk` branch](https://git.sr.ht/~kerrick/ratatui_ruby-tea/tree/trunk) for pre-release updates.
28
28
 
29
+ ---
30
+
31
+ ## Quick Links
32
+
33
+ ### The Ecosystem
34
+
35
+ **RatatuiRuby:** [Core engine](https://git.sr.ht/~kerrick/ratatui_ruby) • **Tea:** [MVU architecture](https://git.sr.ht/~kerrick/ratatui_ruby-tea) • **Kit:** [Component architecture](https://git.sr.ht/~kerrick/ratatui_ruby-kit) (Planned) • **DSL:** [Glimmer syntax](https://sr.ht/~kerrick/ratatui_ruby/#chapter-4-the-syntax) (Planned) • **Framework:** [Omakase framework](https://git.sr.ht/~kerrick/ratatui_ruby-framework) (Planned) • **UI:** [Polished widgets](https://git.sr.ht/~kerrick/ratatui_ruby-ui) (Planned) • **UI Pro:** [More polished widgets](https://sr.ht/~kerrick/ratatui_ruby#chapter-6-licensing) (Planned)
36
+
37
+ ### For App Developers
38
+
39
+ **Get Started:** [Quickstart](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/doc/getting_started/quickstart.md) • [Examples](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [FAQ](https://man.sr.ht/~kerrick/ratatui_ruby/troubleshooting.md) ⸺ **Reach Out:** [Discuss List](https://lists.sr.ht/~kerrick/ratatui_ruby-discuss) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
40
+
41
+ ### For Contributors
42
+
43
+ **Get Started:** [Contributing Guide](https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md) • [Code of Conduct](https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [Project History](https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md) ⸺ **Reach Out:** [Development List](https://lists.sr.ht/~kerrick/ratatui_ruby-devel) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
44
+
45
+ ---
29
46
 
30
47
  ## Compatibility
31
48
 
data/Rakefile CHANGED
@@ -13,4 +13,4 @@ RatatuiRuby::Devtools.install!
13
13
  # Import project-specific tasks
14
14
  Dir.glob("tasks/*.rake").each { |r| import r }
15
15
 
16
- task default: %w[lint:fix test lint reuse]
16
+ task default: %w[lint:fix test lint reuse steep]
data/Steepfile CHANGED
@@ -7,7 +7,7 @@ target :lib do
7
7
  signature "sig"
8
8
  check "lib"
9
9
 
10
- library "pathname"
11
- library "fileutils"
12
- library "minitest"
10
+ library "open3"
11
+
12
+ collection_config "rbs_collection.yaml"
13
13
  end
@@ -0,0 +1,204 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Custom Commands Design (`ratatui_ruby-tea`)
7
+
8
+ This document describes the architectural design and guiding principles of custom commands in `ratatui_ruby-tea`. It is intended for contributors, architects, and AI agents working on the codebase.
9
+
10
+ ## Core Abstractions
11
+
12
+ Custom commands extend Tea with user-defined side effects: WebSockets, gRPC, database polling, background workers. The architecture provides four key components:
13
+
14
+ | Component | Purpose |
15
+ |-----------|---------|
16
+ | `Command::Custom` | Mixin for command identification |
17
+ | `Command::Outlet` | Messaging gateway for result delivery |
18
+ | `Command::CancellationToken` | Cooperative cancellation mechanism |
19
+ | `Command.custom { ... }` | Wrapper giving callables unique identity |
20
+
21
+ ---
22
+
23
+ ## Guiding Design Principles
24
+
25
+ ### 1. Messaging Gateway over Raw Queue
26
+
27
+ Commands produce messages. Those messages cross threads. Without abstraction, queue manipulation scatters across the codebase.
28
+
29
+ The Outlet wraps the internal queue with a domain-specific API. Commands call `out.put(:tag, data)` instead of managing queue details. Debug mode validates Ractor-shareability automatically—commands cannot accidentally push unshareable data.
30
+
31
+ | Aspect | Raw Queue | Outlet |
32
+ |--------|-----------|--------|
33
+ | Ractor safety | Manual | Automatic |
34
+ | Error messages | Generic | Contextual |
35
+ | API | `queue << Ractor.make_shareable([...])` | `out.put(:tag, data)` |
36
+ | Pattern | Implementation detail | **Messaging Gateway** |
37
+
38
+ This implements the **Messaging Gateway** pattern from Enterprise Integration Patterns. The Outlet is a **Facade** (Gang of Four) over the queue, and acts as a **Channel Adapter** translating command results into update function messages.
39
+
40
+ ### 2. Cooperative Cancellation over Thread Termination
41
+
42
+ Long-running commands block the event loop. WebSocket listeners, database pollers, and streaming connections run indefinitely until stopped. Stopping them abruptly corrupts state.
43
+
44
+ `Thread#kill` terminates immediately. Mutexes may deadlock. Resources may leak. Database transactions may abort mid-write.
45
+
46
+ The `CancellationToken` signals cancellation requests. Commands check `token.cancelled?` periodically and stop at safe points. Cleanup code runs. Resources release. Transactions commit.
47
+
48
+ | Aspect | `Thread#kill` | CancellationToken |
49
+ |--------|---------------|------------------|
50
+ | Cleanup | None (immediate termination) | Command controls cleanup |
51
+ | Resource safety | May corrupt state | Clean shutdown |
52
+ | Mutexes | May deadlock | Released properly |
53
+ | Pattern | Forceful | **Cooperative** |
54
+
55
+ **Configurable grace periods** accommodate different cleanup needs:
56
+
57
+ - `0.5` seconds — Quick HTTP abort, minimal cleanup
58
+ - `2.0` seconds — Default, suitable for most commands
59
+ - `5.0` seconds — WebSocket close handshake with remote server
60
+ - `Float::INFINITY` — Never force-kill (database transactions)
61
+
62
+ ### 3. Command Identity via Object Reference
63
+
64
+ The runtime tracks active commands by their object identity. When the update function returns `Command.cancel(handle)`, the runtime looks up that exact object in its registry and signals its token.
65
+
66
+ Class-based commands get unique identity automatically—each `MyCommand.new` produces a distinct object. Reusable lambdas and procs share identity. Dispatch them twice, and cancellation would affect both.
67
+
68
+ `Command.custom(callable)` wraps a callable in a unique container. Each call produces a distinct handle. Store it in your model. Cancel it later by returning `Command.cancel(handle)`.
69
+
70
+ ### 4. Automatic Error Propagation
71
+
72
+ Commands run in threads. Exceptions bubble up silently. The update function never sees them. Backtraces written to STDERR corrupt the TUI display.
73
+
74
+ The runtime catches unhandled exceptions and pushes `Command::Error` to the queue. This is automatic—commands do not rescue exceptions unless they want custom handling. The update function pattern-matches on `Command::Error` and reacts appropriately.
75
+
76
+ This mirrors the sentinel pattern used for `Command::Exit` and `Command::Cancel`.
77
+
78
+ ### 5. Ractor Readiness
79
+
80
+ This design is forward-compatible with Ruby's Ractor-based parallelism.
81
+
82
+ **What's already shareable:**
83
+
84
+ | Pattern | Shareable? | Why |
85
+ |---------|------------|-----|
86
+ | Module-constant lambda | ✅ Yes | `self` is the frozen module |
87
+ | Closure-free block | ✅ Yes | No captured variables |
88
+ | `Data.define` command | ✅ Yes | Immutable by definition |
89
+ | Frozen class instance | ✅ Yes | Deeply frozen |
90
+
91
+ **Debug mode validation:**
92
+
93
+ In debug mode, Tea validates Ractor shareability at dispatch time. `Ractor.shareable?(command)` catches most issues. The Outlet's `put` method validates messages before pushing them to the queue.
94
+
95
+ **Why Thread dispatch is Ractor-safe:**
96
+
97
+ Commands execute in Threads within the **main Ractor**. Only messages (via Outlet) cross the shareability boundary. The `CancellationToken` stays within its Thread—never shared across Ractors. The `@active_commands` hash is local to the Runtime (main Ractor).
98
+
99
+ When Ruby evolves to true Ractor parallelism, this design upgrades transparently: commands are already validated as shareable, so they could be sent to worker Ractors without code changes.
100
+
101
+ ---
102
+
103
+ ## Pattern Lineage
104
+
105
+ This design implements several established patterns from the software architecture literature.
106
+
107
+ ### Design Patterns (Gang of Four)
108
+
109
+ | Component | Pattern |
110
+ |-----------|---------|
111
+ | Custom Mixin | **Command** — Encapsulates a request as an object |
112
+ | Outlet | **Facade** — Simplified interface to a complex subsystem |
113
+ | Runtime | **Mediator** — Coordinates command dispatch and message routing |
114
+ | Update Function | **State** — Behavior changes based on internal state |
115
+
116
+ ### Enterprise Integration Patterns (Hohpe & Woolf)
117
+
118
+ | Component | Pattern |
119
+ |-----------|---------|
120
+ | Outlet | **Messaging Gateway** — Wraps message channel access |
121
+ | Outlet | **Channel Adapter** — Connects applications to messaging systems |
122
+ | Queue | **Point-to-Point Channel** — Single consumer receives each message |
123
+
124
+ ### Cancellation Patterns
125
+
126
+ | Platform | Mechanism | Relationship |
127
+ |----------|-----------|--------------|
128
+ | **.NET** | `CancellationToken` | Direct inspiration for API design |
129
+ | **Go** | `context.Context` | Cooperative cancellation via `ctx.Done()` |
130
+ | **JavaScript** | `AbortController` | Web Fetch API cancellation |
131
+ | **Java** | `Thread.interrupt()` | Flag-based cooperative cancellation |
132
+ | **Kotlin** | `Job.cancel()` | Coroutine cancellation |
133
+
134
+ ---
135
+
136
+ ## Prior Art
137
+
138
+ ### The Elm Architecture (TEA)
139
+
140
+ | Framework | Language | Notes |
141
+ |-----------|----------|-------|
142
+ | **Elm** | Elm | Original TEA; `Cmd` + `Sub` primitives |
143
+ | **BubbleTea** | Go | TUI implementation; "Subscriptions are Loops" philosophy |
144
+ | **Iced** | Rust | GUI TEA with Command + Subscription |
145
+ | **Miso** | Haskell | Web TEA with Effect + Sub |
146
+ | **Bolero** | F# | Web TEA with Cmd + Sub |
147
+
148
+ ### Redux Ecosystem
149
+
150
+ | Library | Pattern | Mapping |
151
+ |---------|---------|---------|
152
+ | **Redux Thunk** | Raw dispatch access | Direct queue access (rejected) |
153
+ | **Redux Saga** | `put()` effect dispatches actions | **Outlet.put** ← adopted |
154
+ | **Redux Observable** | RxJS Observables | RxRuby (rejected for complexity) |
155
+ | **redux-loop** | Elm-style Cmd | Recursive commands |
156
+
157
+ Redux Saga's `put()` is the direct inspiration for `out.put()`. The Saga pattern—long-running processes that listen for actions and dispatch new ones—maps directly to Tea's custom commands.
158
+
159
+ ### Ruby Ecosystem
160
+
161
+ | Library | Pattern | Relationship |
162
+ |---------|---------|--------------|
163
+ | **Observable** (stdlib) | Observer pattern | Similar push semantics, lacks thread-safety |
164
+ | **Concurrent Ruby** | Actor, Promises | More complex than needed |
165
+ | **Celluloid** | Actor mailboxes | Inspiration for internal terminology |
166
+ | **Sidekiq** | Background jobs | Similar "fire command, receive result" model |
167
+
168
+ ---
169
+
170
+ ## Key Influences Summary
171
+
172
+ | Influence | What We Adopted |
173
+ |-----------|-----------------|
174
+ | **Elm** | MVU architecture, Cmd/Msg pattern |
175
+ | **BubbleTea** | "Subscriptions are Loops" philosophy |
176
+ | **Redux Saga** | `put()` for message dispatch |
177
+ | **.NET CancellationToken** | Cooperative cancellation with grace periods |
178
+ | **EIP Messaging Gateway** | Outlet as infrastructure abstraction |
179
+ | **Go context.Context** | Cancellation propagation pattern |
180
+
181
+ ---
182
+
183
+ ## Further Reading
184
+
185
+ ### External Resources
186
+
187
+ - [Elm Guide](https://guide.elm-lang.org/) — Official Elm documentation covering commands and effects
188
+ - [BubbleTea](https://github.com/charmbracelet/bubbletea) — Go TUI framework based on The Elm Architecture
189
+ - [Redux Saga](https://redux-saga.js.org/) — Saga pattern and `put()` effect documentation
190
+ - [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) — Messaging patterns reference by Hohpe & Woolf
191
+
192
+ ### Academic References
193
+
194
+ 1. **Gamma, Helm, Johnson, Vlissides** (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
195
+ - Command, Facade, Mediator, State patterns
196
+
197
+ 2. **Fowler, M.** (2002). *Patterns of Enterprise Application Architecture*. Addison-Wesley.
198
+ - Gateway pattern
199
+
200
+ 3. **Hohpe, G. & Woolf, B.** (2003). *Enterprise Integration Patterns*. Addison-Wesley.
201
+ - Messaging Gateway, Message Channel, Point-to-Point Channel, Channel Adapter
202
+
203
+ 4. **Joshi, U.** (2023). *Patterns of Distributed Systems*. Addison-Wesley.
204
+ - Write-Ahead Log, Thread Pool
@@ -0,0 +1,237 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Why Kit Doesn't Need Outlet
8
+
9
+ > **Context**: The `unified-draft.md` specifies `Tea::Command::Outlet` for message passing from custom commands to the Tea runtime. This document explains why Kit (the component-based runtime) does not need this abstraction.
10
+
11
+ ---
12
+
13
+ ## The Core Insight: Immediate-Mode Rendering
14
+
15
+ RatatuiRuby's Engine is **immediate-mode**:
16
+
17
+ > "Each frame, your code describes the entire UI from scratch. The Engine draws it and immediately forgets it, holding no application state between renders."
18
+
19
+ This means Kit walks the entire component tree and calls `render` **every frame**. Components don't need to notify anyone of state changes—the next frame automatically sees updated state.
20
+
21
+ ---
22
+
23
+ ## Tea vs Kit: Different Problems
24
+
25
+ | Concern | Tea | Kit |
26
+ |---------|-----|-----|
27
+ | **State model** | Immutable (Ractor-safe) | Mutable (encapsulated) |
28
+ | **State location** | Single Model in runtime | Distributed in components |
29
+ | **Async results** | Message → Queue → Update | Callback → mutate state |
30
+ | **Render trigger** | After update function | **Every frame** |
31
+ | **Re-render notification** | Implicit (update always renders) | **Not needed** (always renders) |
32
+
33
+ ---
34
+
35
+ ## WebSocket Example: Tea vs Kit
36
+
37
+ ### Tea: Outlet Required
38
+
39
+ ```ruby
40
+ class WebSocketCommand
41
+ include Tea::Command::Custom
42
+
43
+ def call(out, token)
44
+ ws = WebSocket::Client.new(@url)
45
+ ws.on_message { |msg| out.put(:ws, :message, data: msg) }
46
+ ws.connect
47
+
48
+ until token.cancelled?
49
+ sleep 1
50
+ end
51
+
52
+ ws.close
53
+ end
54
+ end
55
+
56
+ def update(msg, model)
57
+ case msg
58
+ in [:ws, :message, data:]
59
+ model.with(messages: model.messages + [data])
60
+ end
61
+ end
62
+ ```
63
+
64
+ Tea needs Outlet because:
65
+ 1. Command runs in a separate thread
66
+ 2. Model is immutable—can't mutate from callback
67
+ 3. Runtime must receive messages to call `update`
68
+
69
+ ### Kit: Direct Mutation
70
+
71
+ ```ruby
72
+ class WebSocketTab
73
+ include Kit::Component
74
+
75
+ def initialize(url:)
76
+ @url = url
77
+ @messages = []
78
+ end
79
+
80
+ def mount
81
+ @ws = WebSocket::Client.new(@url)
82
+ @ws.on_message { |msg| @messages << msg }
83
+ @ws.connect_async
84
+ end
85
+
86
+ def unmount
87
+ @ws&.close
88
+ end
89
+
90
+ def render(frame, area)
91
+ frame.render_widget(tui.list(items: @messages.last(10)), area)
92
+ end
93
+ end
94
+ ```
95
+
96
+ Kit doesn't need Outlet because:
97
+ 1. Component owns the WebSocket directly
98
+ 2. State is mutable—callbacks mutate `@messages`
99
+ 3. Next frame's `render` sees updated state automatically
100
+
101
+ ---
102
+
103
+ ## Why Each Tea Concept Is Unnecessary in Kit
104
+
105
+ ### Outlet (Message Gateway)
106
+
107
+ **Tea**: Routes messages from command thread → runtime queue → update function.
108
+
109
+ **Kit**: Not needed. Callbacks mutate component state directly. Immediate-mode rendering sees changes next frame.
110
+
111
+ ### CancellationToken
112
+
113
+ **Tea**: Runtime signals command to stop cooperatively, since commands run in spawned threads tracked by the runtime.
114
+
115
+ **Kit**: Not needed. Components have `unmount` lifecycle hook. Component stops its own resources:
116
+
117
+ ```ruby
118
+ def unmount
119
+ @ws&.close
120
+ @polling_thread&.kill
121
+ end
122
+ ```
123
+
124
+ ### Ractor Safety
125
+
126
+ **Tea**: Messages cross thread boundaries and must be Ractor-shareable for future Ruby 4.0 compatibility.
127
+
128
+ **Kit**: Not needed. Components are mutable by design. State stays within the component. No Ractor isolation required.
129
+
130
+ ### Thread Tracking
131
+
132
+ **Tea**: Runtime tracks spawned command threads to ensure clean shutdown.
133
+
134
+ **Kit**: Not needed. Each component tracks its own resources. Tree traversal during shutdown calls `unmount` on each component.
135
+
136
+ ---
137
+
138
+ ## What Kit DOES Need
139
+
140
+ ### 1. Lifecycle Hooks
141
+
142
+ ```ruby
143
+ module Kit::Component
144
+ def mount
145
+ # Called when component enters tree
146
+ end
147
+
148
+ def unmount
149
+ # Called when component leaves tree
150
+ end
151
+ end
152
+ ```
153
+
154
+ These replace Tea's command spawning and cancellation.
155
+
156
+ ### 2. Thread Safety for Complex Mutations
157
+
158
+ For simple mutations (array append, boolean toggle), Ruby's GIL is sufficient.
159
+
160
+ For complex mutations:
161
+
162
+ ```ruby
163
+ def mount
164
+ @mutex = Mutex.new
165
+ @ws = WebSocket::Client.new(@url)
166
+ @ws.on_message do |msg|
167
+ @mutex.synchronize { @messages << msg }
168
+ end
169
+ end
170
+
171
+ def render(frame, area)
172
+ messages = @mutex.synchronize { @messages.dup }
173
+ # render with messages
174
+ end
175
+ ```
176
+
177
+ ### 3. Error Handling
178
+
179
+ Components should rescue and surface errors:
180
+
181
+ ```ruby
182
+ def mount
183
+ @ws = WebSocket::Client.new(@url)
184
+ @ws.on_error { |e| @error = e.message }
185
+ rescue => e
186
+ @error = e.message
187
+ end
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Comparison Table
193
+
194
+ | Abstraction | Tea | Kit | Why Different |
195
+ |-------------|-----|-----|---------------|
196
+ | **Outlet** | ✓ Required | ✗ Not needed | Kit mutates directly |
197
+ | **CancellationToken** | ✓ Required | ✗ Not needed | Kit has `unmount` |
198
+ | **Ractor safety** | ✓ Required | ✗ Not needed | Kit is mutable |
199
+ | **Thread tracking** | ✓ Runtime tracks | ✗ Not needed | Components self-manage |
200
+ | **Lifecycle hooks** | ✗ Not applicable | ✓ Required | Kit needs mount/unmount |
201
+ | **Mutex** | ✗ Uses Outlet | ⚠ If complex | Kit needs manual locking |
202
+
203
+ ---
204
+
205
+ ## Architectural Summary
206
+
207
+ ```
208
+ ┌─────────────────────────────────────────────────────────────────┐
209
+ │ ENGINE │
210
+ │ (Immediate-mode, renders every frame) │
211
+ ├─────────────────────────────┬───────────────────────────────────┤
212
+ │ TEA │ KIT │
213
+ │ │ │
214
+ │ Immutable Model │ Mutable Components │
215
+ │ Commands spawn threads │ Components own resources │
216
+ │ Outlet sends messages │ Callbacks mutate state │
217
+ │ Runtime tracks threads │ unmount cleans up │
218
+ │ Ractor-safe required │ GIL + Mutex sufficient │
219
+ │ │ │
220
+ │ NEEDS OUTLET │ DOESN'T NEED OUTLET │
221
+ └─────────────────────────────┴───────────────────────────────────┘
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Conclusion
227
+
228
+ **Outlet is Tea-specific.** It solves the problem of getting async results into an immutable, unidirectional data flow.
229
+
230
+ Kit's paradigm—mutable components with immediate-mode rendering—makes Outlet unnecessary:
231
+
232
+ 1. **Callbacks mutate state** → No message routing needed
233
+ 2. **Render every frame** → No change notification needed
234
+ 3. **`unmount` hook** → No external cancellation needed
235
+ 4. **Components own resources** → No runtime tracking needed
236
+
237
+ The Outlet stays in `Tea::Command::Outlet`. Kit needs no equivalent.