Skip to content

feat: merge reactive TUI, headless mode, and integrations overhaul#2

Merged
ruttydm merged 22 commits intomainfrom
feat/reactive-primitives
Apr 10, 2026
Merged

feat: merge reactive TUI, headless mode, and integrations overhaul#2
ruttydm merged 22 commits intomainfrom
feat/reactive-primitives

Conversation

@ruttydm
Copy link
Copy Markdown
Contributor

@ruttydm ruttydm commented Apr 10, 2026

Summary

Merges the long-lived feat/reactive-primitives branch into main.

Major changes

  • reactive TUI state/animation/rendering overhaul
  • headless CLI mode for non-interactive execution
  • session search, context, retry, and logging improvements
  • integrations/settings workspace overhaul
  • dynamic integration discovery from the integrations monorepo
  • Lua docs workflow cleanup and active-only prompt injection
  • Plane/self-hosted integration hardening and related test coverage
  • website/docs refresh for commands, configuration, tools, permissions, architecture, and headless mode

Validation

  • php vendor/bin/phpstan analyse
  • php vendor/bin/phpunit --stop-on-failure
  • targeted style checks with php vendor/bin/pint --test ...

Full PHPUnit now passes without failures/errors. Existing deprecations/notices remain, but they are not test failures.

ruttydm added 22 commits April 8, 2026 11:34
Phase 1: Signal primitives (Signal, Computed, Effect, EffectScope, BatchScope, Subscriber)
Phase 2: TuiStateStore centralizes all mutable UI state as reactive signals
Phase 3: All sub-managers (Animation, Tool, Modal, Subagent) migrated to store
Phase 4: Imperative refresh calls replaced by effects driving all rendering

- TuiStateStore: 35+ signals, 3 computed values, batch helper
- 4 root effects: status bar, history status, render trigger, task bar
- PhaseStateMachine for validated phase transitions
- BatchScope bug fix: clear current scope before flush so effects execute
- Removed refreshStatusBar(), refreshHistoryStatus(), refreshTaskBarCallback
- addConversationWidget() auto-triggers render
- Only structural widget-tree mutations remain imperative

2513 tests, 0 failures, pint clean
…n hardening

- Move Signal, Computed, Effect, EffectScope, BatchScope, Subscriber
  from Kosmokrator\UI\Tui\Signal\ to OpenCompany\Signal\ namespace
- Add ReadableSignalInterface for read-only signal views
- Make event loop injectable via BatchScope::setScheduler(callable)
  (removed hard Revolt\EventLoop dependency)
- Add EffectScope ownership: effect() + dispose() for auto-cleanup
- Fix Computed exception safety: restore dirty=true on failure, rethrow
- Add Effect cycle detection: depth > 100 throws LogicException
- Document === identity semantics for Signal::set()
- Update composer.json autoload with OpenCompany\ namespace root
- 14 new tests covering all audit fixes

2527 tests, 0 failures, pint clean
Wrap breathing timer bodies in BatchScope::run() to collapse multiple
signal writes (breathTick, breathColor, cachedLoaderLabel) into a
single effect cycle per tick. Previously each signal set() triggered
the task bar effect + flushRender independently — now effects fire once
at batch completion, reducing renders from 3+/tick to 1/tick.

Applied to:
- TuiAnimationManager::startBreathingAnimation() (thinking/tools)
- TuiAnimationManager::startCompactingAnimation()
- SubagentDisplayManager::showRunning() (elapsed timer)
Prepares for future extraction as standalone rubedo/signals package.
Composer autoload maps Rubedo\ → src/Rubedo/.
- SubagentTool: fix batch parameter schema (items type: object)
- SubagentTool: simplify mode validation
- LuaDocService: expand subagent docs with single/batch/background examples
- Lua overview: add subagent tool section with per-agent options
- SubagentToolTest: add batch validation and execution tests
- Add docs/plans/tui-overhaul/ planning documents
- Deep audits: error handling, logic bugs, resource management, session persistence
- PHP file audit and website docs audit
- Swarm scale subagents proposal
- Updated website docs: agents, architecture, commands, configuration, context,
  getting-started, installation, patterns, permissions, providers, tools, ui-guide
- TUI modal manager and tool renderer updates
- Local config override
…ipting

New files:
- src/UI/OutputFormat.php — text/json/stream-json enum
- src/UI/HeadlessRenderer.php — full RendererInterface for stdout/stderr
- src/Agent/Exception/MaxTurnsExceededException.php
- src/Agent/Exception/TimeoutExceededException.php
- website/pages/docs/headless.php — full docs with examples

Modified files:
- src/Command/AgentCommand.php — 15+ CLI options (-p, -o, -m, --yolo,
  --max-turns, --timeout, -c, etc.), headless detection, runHeadless()
- src/Agent/AgentSessionBuilder.php — buildHeadless() method, extracted
  buildLuaDocsSuffix() to DRY
- src/Agent/AgentLoop.php — fixed 3 bugs (FinishReason::Length continuation,
  \Amp\delay(0) yielding, streamComplete()), added maxTurns/timeout guardrails
- src/Agent/AgentSession.php — widened $ui to RendererInterface
- src/Agent/LlmClientFactory.php — widened to RendererInterface
- src/Agent/SubagentPipelineFactory.php — widened to RendererInterface
- src/Command/SlashCommandContext.php — widened to RendererInterface
- src/Skill/SkillDispatcher.php — widened to RendererInterface
- bin/kosmokrator — fixed single-command mode for positional prompts

Website docs:
- New headless page with 12 sections (quick start, output formats, CLI
  reference, CI/CD integration, scripting patterns, migration guide)
- Added to sidebar and index navigation
- Cross-linked from permissions, agents, patterns, ui-guide pages
- Rebuilt all static HTML

Audit fixes (2 rounds):
- HIGH: emitError() JSON mode now writes to stderr, not stdout
- HIGH: Error strings from runHeadless() detected → exit code 1
- MEDIUM: ValueError catch for invalid enum options
- MEDIUM: --continue works in interactive mode too
- MEDIUM: Piped stdin triggers headless automatically
- MEDIUM: showUserMessage() call for StreamJson consumers
- MEDIUM: Consistent error schema with timestamps in JSON mode
- MEDIUM: Real token counts in JSON output
- MEDIUM: posix_isatty() extension guard
- MEDIUM: JSON_INVALID_UTF8_SUBSTITUTE + json_encode fallback
- MEDIUM: setMaxTurns/setTimeout validation (>= 1)
- MEDIUM: SkillDispatcher type hint widened
- LOW: SIGTERM exit code corrected to 143
- LOW: Lua docs block DRYed up
…mers

Phase 1-9 of the reactive TUI primitives migration:

- Add 4 signals to TuiStateStore: toolExecutingBreathTick,
  toolExecutingStartTime, hasThinkingLoader, hasCompactingLoader
- Extract StatusBarBuilder (status bar setup + reactive update)
- Extract TaskBarBuilder (task tree rendering from signals + TaskStore)
- Extract ToolExecutionCard (tool execution animation with own 20fps timer)
- Extract BreathingDriver (single 33ms timer for thinking + compacting)
- Remove breathColorProvider/renderCallback closures from
  SubagentDisplayManager (reads signals directly)
- Remove 2 independent timers from TuiAnimationManager, delegate to
  BreathingDriver
- Replace manual Effect array with EffectScope for lifecycle management
- Remove 1 redundant double-render call in TuiToolRenderer
- Add 16 new builder tests, 4 new signal tests

581 TUI tests pass, no regressions.
- RetryableLlmClient: classify errors (rate-limited, server error, network
  error, provider overloaded), log provider/model/total_wait context, escalate
  to error level after 5 attempts
- TuiToolRenderer: clear activeDiscoveryItems in finalizeDiscoveryBatch()
…ring

Adds the declarative UI primitive layer and wires it into TuiCoreRenderer:

Primitive layer (src/UI/Tui/Primitive/):
- ReactiveWidget: base class using beforeRender() → syncFromSignals() → invalidate()
- ReactiveBridge: single Effect replacing scattered flushRender/triggerRender calls
- Layout: VStack, HStack, Spacer (SwiftUI-style factory methods)
- Display: Text, Sep, ContextMeter, Loader, Markdown (signal-bound widgets)
- Collection: When, ReactiveList (conditional + keyed list reconciliation)

Composition layer (src/UI/Tui/Composition/):
- StatusBar: declarative status bar with formatTokenDetail/formatRuntimeDetail
- TaskTree: self-contained ReactiveWidget replacing TaskBarBuilder

TuiCoreRenderer changes:
- TaskTree widget replaces TaskBarBuilder widget in layout
- StatusBar composition replaces StatusBarBuilder for sync/format calls
- ReactiveBridge starts alongside existing Effects (parallel operation)
- All 2555 tests pass
Replaced by Composition\StatusBar and Composition\TaskTree.
ToolExecutionCard remains (used by TuiToolRenderer).
The stop() method disposes the EffectScope, making it unusable.
Create a fresh scope in start() instead of reusing the disposed one.
scope is null on first start() call — use null-safe dispose().
- Reads scrollOffset and hasHiddenActivityBelow signals via syncFromSignals()
- Removes the HistoryStatus Effect from TuiCoreRenderer::initialize()
- Removes renderTrigger Effect — ReactiveBridge handles requestRender()
- Down to 1 Effect (status bar sync) + ReactiveBridge
- Rewrites HistoryStatusWidgetTest for signal-based API
- Makes syncFromSignals() public for testability
- ReactiveStatusBar wraps ProgressBarWidget, self-syncs via beforeRender()
- Removes the status bar sync Effect
- Removes the renderTrigger Effect
- Removes the EffectScope entirely (was only holding Effects)
- Zero Effects remain in TuiCoreRenderer::initialize()
- ReactiveBridge now tracks tokensIn, maxContext, renderTrigger signals
- showStatus/refreshRuntimeSelection just set signals, no manual sync
- streamChunk no longer calls triggerRender (ReactiveBridge handles it)
Major refactor: TuiAnimationManager no longer manages CancellableLoaderWidget
instances directly. It sets signals only.

New reactive widgets:
- ThinkingLoaderWidget: self-managing CancellableLoaderWidget lifecycle
  via hasThinkingLoaderSignal. Mounts/unmounts based on signal changes.
- CompactingLoaderWidget: same pattern for compacting loader.

Changes:
- TuiAnimationManager: removed thinkingBar container parameter, removed
  loader/compactingLoader properties, removed getLoader() method
- BreathingDriver: signal-only, no longer manages CancellableLoaderWidget
  messages directly. Only ticks breath counters and computes colors.
- TuiCoreRenderer: loaders are now reactive widgets in the session layout
- Removed ContainerWidget $thinkingBar from the layout entirely
- Rewrote TuiAnimationManagerTest for signal-based API
- All 2537 tests pass
New test files:
- TextTest (21 tests): static/reactive text, color, bold, dim, truncation
- ContextMeterTest (5 tests): 0/50/100% bars, change detection, custom width
- SepTest (4 tests): pipe separator, full-width line, custom char, zero width
- LayoutTest (7 tests): VStack/HStack children + classes, Spacer flex/render
- WhenTest (4 tests): show returns binding, attach/detach, initial state
- ReactiveBridgeTest (2 tests): start/stop lifecycle, idempotent stop
- ReactiveWidgetTest (2 tests): beforeRender calls syncFromSignals every frame
- ReactiveStatusBarTest (9 tests): create, sync, change detection, render
- TaskTreeTest (9 tests): empty/populated store, render output, setTaskStore
FileReadTool's read cache returns an 'Unchanged since last read' stub
on repeated calls, which is useful for the LLM's normal tool loop but
breaks Lua scripts that call app.tools.file_read() multiple times.

Changes:
- FileReadTool: add optional 'fresh' parameter to skip cache
- NativeToolBridge: auto-set fresh=true for file_read calls from Lua
The read cache returned '[Unchanged since last file_read...] on repeat
calls, saving tokens but causing real problems:

- Broken after compaction (original content gone from context)
- Broke Lua scripts (app.tools.file_read returned stub, not data)
- Broke subagents (same issue, fresh=true was a band-aid)
- Surprising behavior (file_read should always read the file)

Removed: readCache, resetCache(), fresh parameter, buildCacheKey(),
formatUnchangedResult(), UNCHANGED_RESULT_TEMPLATE.

Also removed NativeToolBridge fresh=true workaround (no longer needed).
Removed 3 cache-specific tests, replaced with test_repeated_reads_return_full_content.
…layers, cleanup audits

- Add SessionSearchTool for searchable session history
- Add src/Logging/ and src/Security/ namespaces
- Enhance ContextCompactor, MemorySelector, MemoryInjector
- Extend MessageRepository and Database with richer queries
- Improve ThinkingLoaderWidget and CompactingLoaderWidget composition
- Update AgentLoop with session search integration
- Remove stale audit docs
- Add comprehensive tests for all new/changed components
@ruttydm ruttydm merged commit 5af633d into main Apr 10, 2026
0 of 3 checks passed
@Bratatat Bratatat mentioned this pull request Apr 14, 2026
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