add Up/Down prompt history to the Claude composer

T-163. Up/Down walk previously-sent prompts, Claude-CLI-style: Up steps
to older entries only once the caret is on the first line (so multi-line
edits move line-by-line first), Down returns toward newer ones and, past
the newest, restores the in-progress draft that was stashed on entry.

History is per-session, owned by the pane (appended on submit, deduping
immediate repeats) and read by the composer. Previewing entries is
suppressed from the draft report, so browsing history never overwrites
the persisted draft (T-228) — and a remount mid-browse restores the real
draft, not a preview. History nav only applies when the slash typeahead
is closed; Up/Down still drive the typeahead when it's open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 12:04:59 +02:00
co-authored by Claude Opus 4.8
parent b4a02dc533
commit ab5d63debd
6 changed files with 220 additions and 2 deletions
@@ -72,3 +72,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-228', 'parent_id', NULL, 'T-229', NULL, '2026-06-03 09:51:40', '2026-06-03 09:51:40', '2026-06-03 09:51:40', NULL, '17b018dd9f5b4c6234dad6074155f4fc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-163', 'parent_id', 'T-132', 'T-229', NULL, '2026-06-03 09:51:40', '2026-06-03 09:51:40', '2026-06-03 09:51:40', NULL, '5dc4470123514f2886b99d983c3f14f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-228', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:54:14', '2026-06-03 09:54:14', '2026-06-03 09:54:14', NULL, 'e9fad0773bf9d930028187db8f219d9f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-163', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 10:02:02', '2026-06-03 10:02:02', '2026-06-03 10:02:02', NULL, 'a5f405a38487027074c841401ab2d45c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-228', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:02:02', '2026-06-03 10:02:02', '2026-06-03 10:02:02', NULL, 'de2818775d8d462446c341b2e74aed2e', 1) ON CONFLICT(hash) DO NOTHING;
+20
View File
@@ -131,3 +131,23 @@ Acceptance:
4. Selection/caret position preserved, not just the string (nice-to-have).
Implementation sketch: store the draft (text + selection) on the per-session model/controller (kernel- or session-scoped), not in the composer widget''s State. Seed the TextEditingController from it on mount and write back on change, so a remount after the interaction zone closes restores it. Relates to T-227 (focusing the composer) and the interaction-zone behavior where prompts replace the composer rather than rendering inline.', 'in_progress', 'high', NULL, NULL, NULL, '2026-06-03 09:49:35', '2026-06-03 09:54:14', NULL, '768ef9874198e50fa92f208e4ada79e0', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-163', 'task', 'T-229', 'Composer prompt history via Up/Down (Claude-CLI-style)', 'Arrow Up/Down in the Claude composer should scroll through previously-sent prompts, matching the Claude Code CLI UX:
- Up: if the caret isn''t on the first visual line, move up one line (normal editing); once on the first line, Up steps to the previous prompt in history. Down mirrors it (move down within multiline; at the last line, step to the next/newer entry).
- When entering history, stash the current draft as the newest entry so Down past the most recent restores exactly what the user was typing.
- History is the sequence of prompts submitted from this composer (this session at least; persisting across restarts is a nice-to-have).
- Plays well with the existing Enter-submit / Shift+Enter-newline and the slash typeahead (Up/Down already drive the typeahead when it''s open — history nav only applies when the typeahead is closed).
Implementation: a small history controller + caret-line detection in ClaudeComposer (EditableText); the Up/Down handling lives alongside the existing _onKey/Shortcuts. Acceptance: send a few prompts, Up walks back through them, Down returns through them to the restored draft; multiline prompts navigate line-by-line before crossing into history; widget tests for the bound conditions (first/last line) and draft stash/restore.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-24 09:42:44', '2026-06-03 10:02:02', NULL, '12f258c45979300ed8c0ecc52ebbe87e', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-228', 'bug', 'T-229', 'Composer draft lost when the Claude pane UI changes', 'Text typed into the Claude composer is lost whenever the pane UI changes underneath it. The clearest trigger: a permission prompt / AskUserQuestion arrives and the interaction zone replaces the composer; when it resolves and the composer returns, the half-typed message is gone. It can also drop on any pane rebuild/refresh (new transcript message, status change, tab re-render) if the draft lives only in ephemeral widget State.
Make the draft persistent: an unsent composer message must survive UI changes and come back intact.
Likely cause: the composer''s TextEditingController lives in widget State that is disposed/recreated when the pane rebuilds or the interaction zone swaps the composer out. The draft (text + selection) needs to be hoisted somewhere that outlives the widget.
Acceptance:
1. Type in the composer, trigger a permission prompt (composer replaced by the interaction zone), resolve it -> the composer returns with the typed text and caret intact.
2. The draft survives an arbitrary Claude-pane rebuild (incoming transcript message, status update, sub-tab switch) without loss.
3. Draft is per-session: switching between Claude sessions/panes preserves each session''s own unsent text; sending or explicitly clearing empties it.
4. Selection/caret position preserved, not just the string (nice-to-have).
Implementation sketch: store the draft (text + selection) on the per-session model/controller (kernel- or session-scoped), not in the composer widget''s State. Seed the TextEditingController from it on mount and write back on change, so a remount after the interaction zone closes restores it. Relates to T-227 (focusing the composer) and the interaction-zone behavior where prompts replace the composer rather than rendering inline.', 'done', 'high', NULL, NULL, NULL, '2026-06-03 09:49:35', '2026-06-03 10:02:02', NULL, 'bdd509507b199031cb2b368a2813245a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);