remove dead Scrollable plumbing from TerminalView

TerminalView's build tree doesn't wrap content in a Scrollable —
scroll is handled by translating PointerScrollEvent into PgUp/PgDown
keyInput. The ScrollController parameter, _scrollableKey, internal
_scrollController, _scrollToBottom helper, and its five call sites
were all dead: _scrollableKey.currentState was always null because no
Scrollable in the tree carried the key, so _scrollToBottom's jumpTo
never fired.

Drops:
- public scrollController parameter on TerminalView
- _scrollableKey + _scrollController fields
- the didUpdateWidget swap block and dispose call
- _scrollToBottom + the five call sites
- KeyboardVisibilty wrapper (its only callback was _scrollToBottom,
  now a no-op; the widget remains a reusable primitive under ui/ for
  future use)
- the matching tests in terminal_view_test.dart

Same shape as T-93 (dead onTapUp wiring) and T-95 (dead tertiary tap
surface) — public API that no caller used + internal state that no
path executed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 18:12:23 +02:00
co-authored by Claude Opus 4.7
parent f46ab50ff9
commit 048e835b31
3 changed files with 120 additions and 81 deletions
+117
View File
@@ -102,3 +102,120 @@ pql ticket board # kanban view of tickets
3. Run `pql decisions validate` (also runs in `make push-check`).
4. Commit. The SQLite index rebuilds from markdown on any
`pql decisions sync`.
<!-- pql:records (auto-generated; do not edit manually) -->
## Decisions
- [D-1: CLI-first, not MCP](architecture.md#d-1-cli-first-not-mcp) — _architecture_
- [D-3: pql as supporter tool; clide wraps, never duplicates](architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates) — _architecture_
- [D-4: Ignore file strategy](architecture.md#d-4-ignore-file-strategy) — _architecture_
- [D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer](architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) — _architecture_
- [D-6: CLI and event surface contract](architecture.md#d-6-cli-and-event-surface-contract) — _architecture_
- [D-7: App root is bare `WidgetsApp`](architecture.md#d-7-app-root-is-bare-widgetsapp) — _architecture_
- [D-8: Feature-first folder layout](architecture.md#d-8-feature-first-folder-layout) — _architecture_
- [D-9: Three-tier theme pipeline](architecture.md#d-9-three-tier-theme-pipeline) — _architecture_
- [D-10: State management — `ChangeNotifier` + `ListenableBuilder`](architecture.md#d-10-state-management--changenotifier--listenablebuilder) — _architecture_
- [D-11: Panel manager is kernel; layout is data; three-column is a preset](architecture.md#d-11-panel-manager-is-kernel-layout-is-data-three-column-is-a-preset) — _architecture_
- [D-12: Kernel admission rule — mandatory shared singletons only](architecture.md#d-12-kernel-admission-rule--mandatory-shared-singletons-only) — _architecture_
- [D-13: Git hardcoded in kernel project-loader](architecture.md#d-13-git-hardcoded-in-kernel-project-loader) — _architecture_
- [D-14: Two-tier disable — kernel locked, everything else extension-shaped](architecture.md#d-14-two-tier-disable--kernel-locked-everything-else-extension-shaped) — _architecture_
- [D-15: Extension grain — container-level, multi-contribution](extensions.md#d-15-extension-grain--container-level-multi-contribution) — _extensions_
- [D-16: Built-ins in Dart, third-party in sandboxed Lua](extensions.md#d-16-built-ins-in-dart-third-party-in-sandboxed-lua) — _extensions_
- [D-17: Panels are extension-shaped from day one](extensions.md#d-17-panels-are-extension-shaped-from-day-one) — _extensions_
- [D-18: YAML for themes + manifests; JSON for i18n catalogs](extensions.md#d-18-yaml-for-themes--manifests-json-for-i18n-catalogs) — _extensions_
- [D-19: Lua runtime as `ptyc`-peer supporter tool](extensions.md#d-19-lua-runtime-as-ptyc-peer-supporter-tool) — _extensions_
- [D-20: A11y is a Tier-0 contract](accessibility.md#d-20-a11y-is-a-tier-0-contract) — _accessibility_
- [D-21: i18n is a Tier-0 contract (fframe pattern + locale-fallback chain)](accessibility.md#d-21-i18n-is-a-tier-0-contract-fframe-pattern--locale-fallback-chain) — _accessibility_
- [D-22: WCAG-AA contrast gate on bundled themes](accessibility.md#d-22-wcag-aa-contrast-gate-on-bundled-themes) — _accessibility_
- [D-23: Test pyramid — seven layers](testing.md#d-23-test-pyramid--seven-layers) — _testing_
- [D-24: Golden tests — primitives only, Alchemist + Ahem](testing.md#d-24-golden-tests--primitives-only-alchemist--ahem) — _testing_
- [D-25: Mocks — mocktail at IO, hand-rolled fakes for ChangeNotifiers](testing.md#d-25-mocks--mocktail-at-io-hand-rolled-fakes-for-changenotifiers) — _testing_
- [D-26: Web driver — raw Playwright + Flutter semantics](testing.md#d-26-web-driver--raw-playwright--flutter-semantics) — _testing_
- [D-27: Startup regression gate](testing.md#d-27-startup-regression-gate) — _testing_
- [D-28: Test organisation — mirror `lib/` in `test/`](testing.md#d-28-test-organisation--mirror-lib-in-test) — _testing_
- [D-29: Pre-push gate — fast layer only](testing.md#d-29-pre-push-gate--fast-layer-only) — _testing_
- [D-30: Tests are client-side only](testing.md#d-30-tests-are-client-side-only) — _testing_
- [D-31: Prefer-zero-deps, exact-pin](tooling.md#d-31-prefer-zero-deps-exact-pin) — _tooling_
- [D-32: CI — Gitea primary, Linux-only runners, not yet activated](tooling.md#d-32-ci--gitea-primary-linux-only-runners-not-yet-activated) — _tooling_
- [D-33: Golden-output ignore pattern — `coverage.*` excludes output, not scripts](tooling.md#d-33-golden-output-ignore-pattern--coverage-excludes-output-not-scripts) — _tooling_
- [D-34: Q&D record system](process.md#d-34-qd-record-system) — _process_
- [D-35: Kanban / waterfall, not Scrum](process.md#d-35-kanban--waterfall-not-scrum) — _process_
- [D-36: `.claude/` is committed project surface, managed through the IDE](process.md#d-36-claude-is-committed-project-surface-managed-through-the-ide) — _process_
- [D-37: Commit conventions per git-commit skill](process.md#d-37-commit-conventions-per-git-commit-skill) — _process_
- [D-38: Changelog discipline — Keep a Changelog 1.1.0](process.md#d-38-changelog-discipline--keep-a-changelog-110) — _process_
- [D-39: Planning tooling lives in pql, not clide](process.md#d-39-planning-tooling-lives-in-pql-not-clide) — _process_
- [D-40: [SUPERSEDED] Python stopgap under `tools/scripts/plan`](process.md#d-40-superseded-python-stopgap-under-toolsscriptsplan) — _process_
- [D-41: Claude panes — one primary per repo, tmux-backed](architecture.md#d-41-claude-panes--one-primary-per-repo-tmux-backed) — _architecture_
- [D-42: Dependencies documented in `licenses.yaml`](tooling.md#d-42-dependencies-documented-in-licensesyaml) — _tooling_
- [D-43: Design handoff — adopt token palettes, reject Material wrapper](architecture.md#d-43-design-handoff--adopt-token-palettes-reject-material-wrapper) — _architecture_
- [D-44: Four bundled themes — clide, midnight, paper, terminal](architecture.md#d-44-four-bundled-themes--clide-midnight-paper-terminal) — _architecture_
- [D-45: Syntax highlighting tokens in the theme pipeline](architecture.md#d-45-syntax-highlighting-tokens-in-the-theme-pipeline) — _architecture_
- [D-46: Core frame builtins vs shipped extensions boundary](extensions.md#d-46-core-frame-builtins-vs-shipped-extensions-boundary) — _extensions_
- [D-47: Interaction model — Claude-is-home layout](architecture.md#d-47-interaction-model--claude-is-home-layout) — _architecture_
- [D-48: Chrome budget — no tabs, no breadcrumbs, keyboard-first](architecture.md#d-48-chrome-budget--no-tabs-no-breadcrumbs-keyboard-first) — _architecture_
- [D-49: Editor mode — inline above Claude, viewer swap](architecture.md#d-49-editor-mode--inline-above-claude-viewer-swap) — _architecture_
- [D-50: Context auto-behavior — right panel reacts to Claude](architecture.md#d-50-context-auto-behavior--right-panel-reacts-to-claude) — _architecture_
- [D-51: Panel collapse — 12px spine with badge](architecture.md#d-51-panel-collapse--12px-spine-with-badge) — _architecture_
- [D-52: Focus mode — full-window takeover](architecture.md#d-52-focus-mode--full-window-takeover) — _architecture_
- [D-53: State persistence across sessions](architecture.md#d-53-state-persistence-across-sessions) — _architecture_
- [D-54: Keyboard map — canonical shortcuts](architecture.md#d-54-keyboard-map--canonical-shortcuts) — _architecture_
- [D-55: Claude pane internal tabs for multi-session](architecture.md#d-55-claude-pane-internal-tabs-for-multi-session) — _architecture_
- [D-56: Dissolve daemon process; Flutter app hosts IPC server](architecture.md#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server) — _architecture_
- [D-57: Frameless custom chrome with per-column 24px hats](architecture.md#d-57-frameless-custom-chrome-with-per-column-24px-hats) — _architecture_
- [D-58: Format engines are adoptable dependencies](tooling.md#d-58-format-engines-are-adoptable-dependencies) — _tooling_
- [D-59: Bundled git via dugite-native](tooling.md#d-59-bundled-git-via-dugite-native) — _tooling_
- [D-60: No network on default launch path](tooling.md#d-60-no-network-on-default-launch-path) — _tooling_
- [D-61: Dependency vetting checklist](tooling.md#d-61-dependency-vetting-checklist) — _tooling_
- [D-62: Dependency removal process](tooling.md#d-62-dependency-removal-process) — _tooling_
- [D-63: Vendored binary rebuild process](tooling.md#d-63-vendored-binary-rebuild-process) — _tooling_
- [D-64: No telemetry — architectural commitment](architecture.md#d-64-no-telemetry--architectural-commitment) — _architecture_
- [D-65: License compatibility matrix](tooling.md#d-65-license-compatibility-matrix) — _tooling_
- [D-66: Line coverage gate at 95%, ratcheted from current](testing.md#d-66-line-coverage-gate-at-95-ratcheted-from-current) — _testing_
## Open questions
- [Q-1: Authorisation granularity on the IPC socket](questions-architecture.md#q-1-authorisation-granularity-on-the-ipc-socket) — _architecture_
- [Q-2: Back-pressure on event streams](questions-architecture.md#q-2-back-pressure-on-event-streams) — _architecture_
- [Q-3: Event persistence + audit/undo](questions-architecture.md#q-3-event-persistence--auditundo) — _architecture_
- [Q-4: `.canvas` schema compatibility with Obsidian](questions-architecture.md#q-4-canvas-schema-compatibility-with-obsidian) — _architecture_
- [Q-5: IPC wire-format stability + `schema_version:`](questions-architecture.md#q-5-ipc-wire-format-stability--schema-version) — _architecture_
- [Q-6: Window chrome — native frame vs frameless custom](questions-architecture.md#q-6-window-chrome--native-frame-vs-frameless-custom) — _architecture_
- [Q-7: macOS app bundle signing / notarisation](questions-architecture.md#q-7-macos-app-bundle-signing--notarisation) — _architecture_
- [Q-8: Extension API shape — widgets, subcommands, both?](questions-extensions.md#q-8-extension-api-shape--widgets-subcommands-both) — _extensions_
- [Q-9: Lua runtime vendoring](questions-extensions.md#q-9-lua-runtime-vendoring) — _extensions_
- [Q-10: Extension manifest `schema_version:`](questions-extensions.md#q-10-extension-manifest-schema-version) — _extensions_
- [Q-11: Coverage gates — hard thresholds vs soft reporting](questions-testing.md#q-11-coverage-gates--hard-thresholds-vs-soft-reporting) — _testing_
- [Q-12: Screen-reader automation (axe-core via Playwright)](questions-testing.md#q-12-screen-reader-automation-axe-core-via-playwright) — _testing_
- [Q-13: Web production-mode a11y](questions-accessibility.md#q-13-web-production-mode-a11y) — _accessibility_
- [Q-14: i18n plurals / gender / date-format tooling](questions-accessibility.md#q-14-i18n-plurals--gender--date-format-tooling) — _accessibility_
- [Q-15: Editor tab — full LSP vs tree-sitter-only highlight](questions-process.md#q-15-editor-tab--full-lsp-vs-tree-sitter-only-highlight) — _process_
- [Q-16: `tree-sitter-dart` grammar maintenance](questions-process.md#q-16-tree-sitter-dart-grammar-maintenance) — _process_
- [Q-17: Icon set growth](questions-process.md#q-17-icon-set-growth) — _process_
- [Q-18: Theme hot-reload in release builds](questions-process.md#q-18-theme-hot-reload-in-release-builds) — _process_
- [Q-19: (withdrawn)](questions-process.md#q-19-withdrawn) — _process_
- [Q-20: Kernel DB service — namespaced SQL access?](questions-process.md#q-20-kernel-db-service--namespaced-sql-access) — _process_
- [Q-21: Pql absorbs planning vs keeps separate](questions-architecture.md#q-21-pql-absorbs-planning-vs-keeps-separate) — _architecture_
- [Q-22: Ticket persistence strategy](questions-architecture.md#q-22-ticket-persistence-strategy) — _architecture_
- [Q-23: SSH-remote development — run clide against a remote workspace](questions-architecture.md#q-23-ssh-remote-development--run-clide-against-a-remote-workspace) — _architecture_
- [Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code](questions-architecture.md#q-25-body-text-face--mono-everywhere-vs-josefin-sans-ui--mono-code) — _architecture_
- [Q-26: Small screen layout (< 1000px)](questions-architecture.md#q-26-small-screen-layout--1000px) — _architecture_
- [Q-27: Two-editor split](questions-architecture.md#q-27-two-editor-split) — _architecture_
- [Q-28: Terminal strip scope — shell only or logs/errors/tests](questions-architecture.md#q-28-terminal-strip-scope--shell-only-or-logserrorstests) — _architecture_
- [Q-29: Branch picker location](questions-architecture.md#q-29-branch-picker-location) — _architecture_
- [Q-30: Focus behavior when editor is dirty and viewer is peeked](questions-architecture.md#q-30-focus-behavior-when-editor-is-dirty-and-viewer-is-peeked) — _architecture_
- [Q-31: XWayland fallback for frameless — proper Wayland protocol needed](questions-architecture.md#q-31-xwayland-fallback-for-frameless--proper-wayland-protocol-needed) — _architecture_
## Rejected
- [R-2: Go sidecar](rejected.md#r-2-go-sidecar) — _rejected_
- [R-3: `MaterialApp` root](rejected.md#r-3-materialapp-root) — _rejected_
- [R-4: Flutter `intl` + ARB codegen for i18n](rejected.md#r-4-flutter-intl--arb-codegen-for-i18n) — _rejected_
- [R-5: Patrol test runner](rejected.md#r-5-patrol-test-runner) — _rejected_
- [R-6: Nerd-font glyph icons](rejected.md#r-6-nerd-font-glyph-icons) — _rejected_
- [R-7: `CupertinoApp` root](rejected.md#r-7-cupertinoapp-root) — _rejected_
- [R-8: Riverpod / Provider / BLoC for state](rejected.md#r-8-riverpod--provider--bloc-for-state) — _rejected_
- [R-9: Port planning tooling into clide](rejected.md#r-9-port-planning-tooling-into-clide) — _rejected_
- [R-10: Python-script stopgap under `tooling/db/`](rejected.md#r-10-python-script-stopgap-under-toolingdb) — _rejected_
- [R-11: Permanent stopgap](rejected.md#r-11-permanent-stopgap) — _rejected_
- [R-12: MaterialApp wrapper from design handoff](rejected.md#r-12-materialapp-wrapper-from-design-handoff) — _rejected_
+2 -51
View File
@@ -15,7 +15,6 @@ import 'package:clide/src/terminal/src/ui/custom_text_edit.dart';
import 'package:clide/src/terminal/src/ui/gesture/gesture_handler.dart';
import 'package:clide/src/terminal/src/ui/input_map.dart';
import 'package:clide/src/terminal/src/ui/keyboard_listener.dart';
import 'package:clide/src/terminal/src/ui/keyboard_visibility.dart';
import 'package:clide/src/terminal/src/ui/render.dart';
import 'package:clide/src/terminal/src/ui/shortcut/actions.dart';
import 'package:clide/src/terminal/src/ui/shortcut/shortcuts.dart';
@@ -32,7 +31,6 @@ class TerminalView extends StatefulWidget {
this.textStyle = const TerminalStyle(),
this.textScaler,
this.padding,
this.scrollController,
this.autoResize = true,
this.backgroundOpacity = 1,
this.focusNode,
@@ -66,12 +64,9 @@ class TerminalView extends StatefulWidget {
final TextScaler? textScaler;
/// Padding around the inner [Scrollable] widget.
/// Padding around the terminal viewport.
final EdgeInsets? padding;
/// Scroll controller for the inner [Scrollable] widget.
final ScrollController? scrollController;
/// Should this widget automatically notify the underlying terminal when its
/// size changes. [true] by default.
final bool autoResize;
@@ -155,16 +150,12 @@ class TerminalViewState extends State<TerminalView> {
final _customTextEditKey = GlobalKey<CustomTextEditState>();
final _scrollableKey = GlobalKey<ScrollableState>();
final _viewportKey = GlobalKey();
String? _composingText;
late TerminalController _controller;
late ScrollController _scrollController;
RenderTerminal get renderTerminal => _viewportKey.currentContext!.findRenderObject() as RenderTerminal;
void _onPointerSignal(PointerSignalEvent event) {
@@ -186,7 +177,6 @@ class TerminalViewState extends State<TerminalView> {
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_controller = widget.controller ?? TerminalController();
_scrollController = widget.scrollController ?? ScrollController();
_shortcutManager = ShortcutManager(
shortcuts: widget.shortcuts ?? defaultTerminalShortcuts,
);
@@ -207,12 +197,6 @@ class TerminalViewState extends State<TerminalView> {
}
_controller = widget.controller ?? TerminalController();
}
if (oldWidget.scrollController != widget.scrollController) {
if (oldWidget.scrollController == null) {
_scrollController.dispose();
}
_scrollController = widget.scrollController ?? ScrollController();
}
_shortcutManager.shortcuts = widget.shortcuts ?? defaultTerminalShortcuts;
super.didUpdateWidget(oldWidget);
}
@@ -225,9 +209,6 @@ class TerminalViewState extends State<TerminalView> {
if (widget.controller == null) {
_controller.dispose();
}
if (widget.scrollController == null) {
_scrollController.dispose();
}
_shortcutManager.dispose();
super.dispose();
}
@@ -260,13 +241,9 @@ class TerminalViewState extends State<TerminalView> {
keyboardAppearance: widget.keyboardAppearance,
deleteDetection: widget.deleteDetection,
onInsert: _onInsert,
onDelete: () {
_scrollToBottom();
widget.terminal.keyInput(TerminalKey.backspace);
},
onDelete: () => widget.terminal.keyInput(TerminalKey.backspace),
onComposing: _onComposing,
onAction: (action) {
_scrollToBottom();
if (action == TextInputAction.done) {
widget.terminal.keyInput(TerminalKey.enter);
}
@@ -293,11 +270,6 @@ class TerminalViewState extends State<TerminalView> {
child: child,
);
child = KeyboardVisibilty(
onKeyboardShow: _onKeyboardShow,
child: child,
);
child = TerminalGestureHandler(
terminalView: this,
terminalController: _controller,
@@ -384,8 +356,6 @@ class TerminalViewState extends State<TerminalView> {
if (!consumed) {
widget.terminal.textInput(text);
}
_scrollToBottom();
}
void _onComposing(String? text) {
@@ -425,31 +395,12 @@ class TerminalViewState extends State<TerminalView> {
shift: HardwareKeyboard.instance.isShiftPressed,
);
if (handled) {
_scrollToBottom();
}
return handled ? KeyEventResult.handled : KeyEventResult.ignored;
}
void _onKeyboardShow() {
if (_focusNode.hasFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
}
void _onEditableRect(Rect rect, Rect caretRect) {
_customTextEditKey.currentState?.setEditableRect(rect, caretRect);
}
void _scrollToBottom() {
final position = _scrollableKey.currentState?.position;
if (position != null) {
position.jumpTo(position.maxScrollExtent);
}
}
}
class _TerminalView extends LeafRenderObjectWidget {
+1 -30
View File
@@ -43,22 +43,19 @@ void main() {
expect(state.renderTerminal, isA<RenderTerminal>());
});
testWidgets('uses the supplied controller / focusNode / scrollController without disposing them', (tester) async {
testWidgets('uses the supplied controller / focusNode without disposing them', (tester) async {
final t = _OutputRecorder().build();
final controller = TerminalController();
final focus = FocusNode();
final scroll = ScrollController();
addTearDown(() {
controller.dispose();
focus.dispose();
scroll.dispose();
});
await tester.pumpWidget(_host(TerminalView(
t,
controller: controller,
focusNode: focus,
scrollController: scroll,
)));
// Tear down via removing the widget; supplied controllers should NOT
// throw on later use (the state didn't dispose them).
@@ -158,15 +155,6 @@ void main() {
await tester.pumpWidget(_host(TerminalView(t, controller: external)));
expect(find.byType(TerminalView), findsOneWidget);
});
testWidgets('swapping scrollController disposes the old auto-created one', (tester) async {
final t = _OutputRecorder().build();
await tester.pumpWidget(_host(TerminalView(t)));
final external = ScrollController();
addTearDown(external.dispose);
await tester.pumpWidget(_host(TerminalView(t, scrollController: external)));
expect(find.byType(TerminalView), findsOneWidget);
});
});
group('TerminalView — keyboard handle path', () {
@@ -473,21 +461,4 @@ void main() {
expect(r.outputs, isNotEmpty);
});
});
group('TerminalView — keyboard visibility', () {
testWidgets('platform keyboard show fires _onKeyboardShow on the focused view', (tester) async {
final t = _OutputRecorder().build();
await tester.pumpWidget(_host(TerminalView(t, autofocus: true)));
await tester.pump();
// Simulate a virtual-keyboard appearance by raising the bottom view
// insets and triggering a metrics-changed cycle.
tester.view.viewInsets = const FakeViewPadding(bottom: 200, left: 0, right: 0, top: 0);
addTearDown(tester.view.resetViewInsets);
tester.binding.handleMetricsChanged();
await tester.pump();
// _onKeyboardShow only acts when the focus node has focus — verify by
// tearing down without throwing.
await tester.pumpWidget(_host(const SizedBox()));
});
});
}