feat: merge reactive TUI, headless mode, and integrations overhaul#2
Merged
feat: merge reactive TUI, headless mode, and integrations overhaul#2
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges the long-lived
feat/reactive-primitivesbranch intomain.Major changes
Validation
php vendor/bin/phpstan analysephp vendor/bin/phpunit --stop-on-failurephp vendor/bin/pint --test ...Full PHPUnit now passes without failures/errors. Existing deprecations/notices remain, but they are not test failures.