persist the Claude composer draft across UI swaps
T-228. Typing in the composer was lost whenever the input was torn down and rebuilt — most visibly when a permission prompt takes the composer's place (D-78), since the prompt card replaces the composer widget and its TextEditingController went with it. Hoist the draft out of the widget: ClaudeComposer gains initialValue + onDraftChanged, and the pane holds a per-session draft map, seeding the composer on (re)mount and clearing the entry on submit. Keying the composer by session id means switching sessions in a pane swaps to that session's own draft. Text and caret both survive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,8 @@ class ClaudeComposer extends StatefulWidget {
|
||||
this.slashCommandsResolver,
|
||||
this.onInterrupt,
|
||||
this.busy = false,
|
||||
this.initialValue,
|
||||
this.onDraftChanged,
|
||||
});
|
||||
|
||||
/// Called with the composed message (typed text plus attachment `@path`
|
||||
@@ -74,6 +76,16 @@ class ClaudeComposer extends StatefulWidget {
|
||||
/// Whether a turn is in flight; shows the Stop affordance.
|
||||
final bool busy;
|
||||
|
||||
/// Seed value (text + selection) the composer mounts with — the
|
||||
/// persisted per-session draft (T-228). The composer restores this on
|
||||
/// init so an in-progress message survives the composer being torn down
|
||||
/// and rebuilt (e.g. a permission prompt taking its place, D-78).
|
||||
final TextEditingValue? initialValue;
|
||||
|
||||
/// Reports every change to the composer's value so the owner can persist
|
||||
/// the draft. Fires with an empty value on submit/clear (T-228).
|
||||
final ValueChanged<TextEditingValue>? onDraftChanged;
|
||||
|
||||
@override
|
||||
State<ClaudeComposer> createState() => _ClaudeComposerState();
|
||||
}
|
||||
@@ -93,6 +105,12 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Restore the persisted draft (text + caret) before wiring listeners,
|
||||
// so seeding doesn't echo back through onDraftChanged (T-228).
|
||||
final seed = widget.initialValue;
|
||||
if (seed != null && seed.text.isNotEmpty) {
|
||||
_controller.value = seed;
|
||||
}
|
||||
_controller.addListener(_onTextChanged);
|
||||
_focus.addListener(_onFocusChanged);
|
||||
}
|
||||
@@ -109,6 +127,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
|
||||
|
||||
void _onTextChanged() {
|
||||
setState(() {});
|
||||
widget.onDraftChanged?.call(_controller.value);
|
||||
_syncTypeahead();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,13 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
bool _spawned = false;
|
||||
|
||||
/// Per-session composer draft (text + caret), held here so an unsent
|
||||
/// message survives the composer being torn down and rebuilt — e.g. a
|
||||
/// permission prompt taking the composer's place (D-78) or a session
|
||||
/// switch within this pane (T-228). Keyed by claude session id; cleared
|
||||
/// when the message is sent.
|
||||
final Map<String, TextEditingValue> _drafts = {};
|
||||
|
||||
/// This pane's stable key in the session orchestrator (T-169).
|
||||
String get _orchId => widget.isPrimary ? 'primary' : 'secondary-${widget.secondaryIndex}';
|
||||
|
||||
@@ -256,6 +263,19 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
_session?.send(text);
|
||||
}
|
||||
|
||||
/// Persist (or clear) the composer draft for the active session (T-228).
|
||||
/// The composer reports an empty value on submit/clear, which drops the
|
||||
/// entry so a sent message doesn't reappear.
|
||||
void _onDraftChanged(TextEditingValue value) {
|
||||
final id = _sessionId;
|
||||
if (id == null) return;
|
||||
if (value.text.isEmpty) {
|
||||
_drafts.remove(id);
|
||||
} else {
|
||||
_drafts[id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// clide-owned `/fork` (T-172): branch this conversation into a new pane.
|
||||
///
|
||||
/// Delegates to the [onFork] callback supplied by [ClaudeSessionHost] with
|
||||
@@ -366,11 +386,16 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
stream: _session?.busyStream,
|
||||
initialData: _session?.busy ?? false,
|
||||
builder: (context, busySnap) => ClaudeComposer(
|
||||
// Key by session so switching sessions in this pane
|
||||
// remounts the composer with that session's own draft.
|
||||
key: ValueKey('composer-${_sessionId ?? 'pending'}'),
|
||||
enabled: _session != null,
|
||||
busy: busySnap.data ?? false,
|
||||
onInterrupt: _session?.interrupt,
|
||||
onSubmit: _send,
|
||||
pasteResolver: () => resolveClipboardAttachment(const NativeClipboard()),
|
||||
initialValue: _sessionId == null ? null : _drafts[_sessionId],
|
||||
onDraftChanged: _onDraftChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user