ADR-components-008
Split `thinkingLog` into `textViewer` (latest) + `textScroll` (accumulating), one shared `text_viewer` renderer
Context
Four output sink nodes existed for text-like data: textViewer (scalar latest, text_viewer display_type), thinkingLog (accumulating, log_list display_type), actionLog (accumulating, log_list), metrics (scalar, metric_table). Two problems. (1) Latent rendering bug on thinkingLog: LogListField.tsx dispatches entries on entry.action_name (nav-action shape) or entry.type/tool/content (LLM-step shape) β both structured. But LLMCallNode emits response as a plain string (builtin_nodes.py:335, {"response": response or ""}), so every accumulated entry in 5 shipped graphs (navgpt_mp3d, navgpt_ce, mapgpt_mp3d, opennav_habitat, text_analysis_demo) rendered as a colored left-border card with an empty body and the literal undefined as the type label β silently broken. (2) Near-duplication with textViewer: same color, same viewer layout, same TEXT input; the only real axis was accumulate=True. Two nodes with different port names (response vs text), a broken renderer, and confusing naming wasn't pulling its weight. Separately, opennav_habitat.json edge e_decision_to_viewer_thinking had a pre-existing orphan targetHandle: "thought" into thinkingLog (which only exposed a response port); the graph executor silently drops edges whose target port doesn't exist, so that viewer had never received data.
Decision
Collapse to two clearly-named sink nodes sharing one renderer. textViewer ("Text") β unchanged, scalar latest-value display. textScroll ("Scroll Text") β new, replaces thinkingLog; accumulating TEXT history, newest-last, bounded by max_visible (default 20). Both declare a single text: TEXT optional input port for symmetry; the old response port is gone. Both use the same text_viewer display_type. TextViewerField.tsx detects Array.isArray(value) at render time β scalar β single block (original rendering), array β scrollable stack with hairline dividers between entries and a +N earlier badge when trimmed by max_visible. TextScrollSink subclasses _SinkBase and declares accumulate=True on its DisplayField("history", "text_viewer", "", data_key="text", max_visible=20, accumulate=True), routing through the existing vacc_* history path in _SinkBase.execute β _SinkBase itself is unchanged. actionLog and metrics are untouched β they need structured input and distinct renderers (log_list stays for actionLog, which receives ACTION-shaped dicts with action_name/action/step/done). All 5 in-repo graphs migrated: node type: thinkingLog β textScroll and the single inbound edge per graph updated to targetHandle: "text". Node IDs (viewer_thinking) and access-grant IDs (ag_graph_state_viewer_thinking) preserved β IDs are stable across the rename, so the 3 graphs with graph_state grants to this viewer keep working without a further migration. Dead barrel viewerFieldRenderers.tsx deleted (superseded by fields/* in a prior refactor per .claude/log.md:1190). Palette entries added/removed in unifiedCatalog.ts; OUTPUT_NODE_TYPES (unifiedNodeTypes.ts), _TYPE_HEIGHT + VIEWER_TYPES (backend layout.py), nodeColors.ts, ExplorerPanel.tsx icon map all updated.
Alternatives
(a) Fix LogListField to render plain strings gracefully, keep thinkingLog as a separate node β rejected; leaves the taxonomy redundant (two text sinks with different port names, same purpose) and papers over the log_list-on-strings dispatch mismatch rather than moving to a renderer matched to the data shape. (b) Add an accumulate toggle to textViewer and drop thinkingLog entirely (one node, two modes via config) β rejected in favor of two node types: a node's type on the canvas should telegraph its behavior; a hidden config toggle forces the reader to click in to know whether they're looking at history or the latest value. Two backend classes cost nothing β both subclass _SinkBase with a different DisplayField. (c) Add a new text_scroll display_type with its own renderer β rejected; the only visual difference between scalar and array rendering is "stacked blocks with dividers" vs "single block", a trivial conditional in one renderer. Two renderers would duplicate styling and force frontend changes any time the TEXT styling evolved. (d) Keep log_list for textScroll but pre-wrap plain strings into {type: "reasoning", content: str} in the backend β rejected; this forces every upstream TEXT source to know about log_list's entry shape, leaking a display concern into business logic. (e) Name the new node scrollText instead of textScroll β rejected for catalog grouping: textβ¦ prefix keeps the two text sinks adjacent in the sidebar palette alphabetically and under the same mental index.
Rationale
The text_viewer display_type is the right unit of abstraction for plain-string display β scalar and array are just two shapes of the same semantic value. _SinkBase's accumulate flag is already the backend-side expression of "scalar vs array", so TextScrollSink is a 15-line subclass that needs zero new infrastructure. Two canvas-visible node types with clear names (Text / Scroll Text) beats one node with a hidden config toggle: the palette communicates intent, and there's no config UI burden. Preserving node IDs across the type rename means existing state grants resolve unchanged β the rename is user-visible only in the palette and the serialized type field. The opennav_habitat orphan-port bug was a free side-win: forcing every graph's edge through a targeted audit during migration made the stale targetHandle: "thought" visible. Deleting viewerFieldRenderers.tsx was a deferred cleanup noted in .claude/log.md:1190 β this session was a natural home for it. Trade-off explicitly accepted: the thinkingLog node_type is removed without a legacy alias, so any out-of-repo graph referencing it will fail to load; the 5 in-repo graphs are migrated and the change is documented in the log and this ADR for user-owned graphs.
Affected docs
core/architecture.md (output viewer catalog tables), design-docs/canvas-system.md (output node_type table + header diagram), core/codebase-map.md (sink class table; dead barrel row removed), api-reference/base-canvas-node.md (viewer sink list), tutorials/execution-logs.md (viewer table + ASCII diagram), capabilities/real-time-observability.md (viewer node_type tables), core/roadmap.md (changelog)