Skip to content

feat(web): 输入框按 ↑ 调出之前发过的消息 - #912

Merged
tt-a1i merged 3 commits into
openpi-dev:mainfrom
6iKUN6:feat/web-composer-input-history
Nov 9, 2026
Merged

tt-a1i merged 3 commits into
openpi-dev:mainfrom
6iKUN6:feat/web-composer-input-history

Conversation

@6iKUN6

@6iKUN6 6iKUN6 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Web 的输入框不能像 TUI 那样按 ↑ 调出之前发过的消息。

  • Pi 原生的 TUI 输入框可以用 ↑ / ↓ 翻自己发过的消息,改一改或者原样再发一次。
  • Web 输入框(web/ui/src/features/composer/Composer.tsx)里的 ↑ / ↓ 只在斜杠命令菜单打开时用来选命令。菜单关着时按 ↑ 只会移动光标,想复用上一条只能翻对话记录复制粘贴。

两边用起来不一致,从 TUI 换到 Web 的用户会很不习惯。

Value

在 Web 里也能快速重发、微调上一条或者更早的消息,操作和 TUI 一样。

Approach

按键规则:和 Pi 的 TUI 一致

参照 pi-tui 里 Editor.navigateHistory:

  • 按 ↑ 往更早的一条翻。只在这几种情况下生效:输入框是空的;光标在最开头;已经在翻历史了。另外光标必须在第一行,没有选中文字。其他时候 ↑ 还是正常移动光标。
  • 按 ↓ 往更新的一条翻,只在翻历史、并且光标在最后一行时生效。翻过最新一条后,恢复开始翻之前的草稿和光标位置。
  • 调出一条历史后,只要改了内容就退出翻历史的状态,再按 ↑ 会从最新一条重新开始。之后按 Enter 照常发送。
  • 这几种情况不处理,原来的按键行为保持不变:斜杠命令菜单打开时;正在用输入法拼字时(这个判断在原有逻辑里就排在最前面);按着 Shift、Ctrl、Alt 或 Cmd 时。
  • 调出的内容如果以 / 开头,不会自动弹出斜杠命令菜单,免得 ↑ / ↓ 被菜单抢走。

历史从哪来

  • 用当前会话里已加载的用户消息,按从新到旧的顺序。连续两条一样的只算一条,空白消息跳过。
  • 对 setup 请求的处理和"跳到第几轮"导航一样(setupDisplayMessage / isSetupPromptEcho),同一次输入不会出现两次。
  • 不另外存一份历史,以 Pi 的 Session 为准。所以刷新页面、换浏览器以后都还在,切换会话后翻到的是那个会话自己的历史。
  • 很长的消息在页面上只投影了前 12,000 字(带 visibleText 截断标记)。调出这种消息时,先用已有的 /api/session/item 分页取回全文再填进输入框。取不到、取回时会话变了,或者用户已经在输入框里改了内容,就不填,不会把截断后的半截文字当成原文。
  • 取全文期间,只有翻历史的键会等加载完,普通的光标移动照常生效;取失败后加载状态会清掉,下次按 ↑ 会重新去取。
  • 只恢复文字,当时附带的图片和文件不会重新附上。

代码

  • 新文件 web/ui/src/features/composer/prompt-history.ts:从会话记录里提取历史、判断按键是否触发、计算下一个位置。都是纯函数,在 tests/web/prompt-history.test.ts 里单独测。
  • Composer.tsx:在按键处理里,斜杠命令菜单那段之后、Enter 发送之前,加一个 recallPrompt。写入输入框复用已有的 updateDraft,所以草稿的保存和恢复(composer-drafts.ts)照常生效。
  • 没加设置项,没加后端接口,发送流程和 Plan 审批都没动。
  • web/dist 已经重新构建。

范围限制

只翻已经加载到页面上的那部分会话(最多最近 250 条记录)。更早的消息以后可以接 /api/session/prompt-history 分页加载,这次没做。

Validation

基线是上游 0addb73,Pi 1.2.0,环境是 macOS arm64、Node v24.16.0。按 review 意见,纯规则只在单元测试里测一次,浏览器测试只测交互行为。

浏览器层面:tests/web/composer-prompt-history.spec.ts,9 个用例(jsdom)。

用例 修改前 修改后
↑ 按从新到旧调出,连续重复只算一次;↓ 依次往回,最后回到空输入框 ❌ ✅
有没发出的草稿时,↑ 翻历史,↓ 翻回来后草稿原样恢复 ❌ ✅
调出后改了内容再发送,发出去的是改过的内容;改完之后 ↓ 不再翻历史 ❌ ✅
被截断的长消息:调出时先保持空白,分两页取回全文后填进输入框,内容和原文逐字一致 ❌ ✅
被截断的长消息取全文失败时什么都不填,再按 ↑ 会重新取并填进全文 ❌ ✅
长消息取全文期间,不在第一行时 ↑ / ↓ 照常移动光标,只有翻历史的键会等加载完 ❌ ✅
切换会话后,翻到的是新会话的历史 ❌ ✅
按着 Shift、Ctrl、Alt、Cmd,或者正在用输入法拼字时,↑ 不调出历史 ✅ ✅
斜杠命令菜单打开时,↑ / ↓ 仍然用来选命令 ✅ ✅

最后 2 个在修改前就能通过,是用来保证原有行为没被破坏的。"取全文期间照常移动光标"这个用例,在这个 PR 的第一版(31768c6)上也会失败,是 review 指出的问题的回归测试。

单元层面:tests/web/prompt-history.test.ts,4 个用例(node:test),直接测 prompt-history.ts:

  • 只有用户自己发的、非空的消息算历史,从新到旧,连续重复只算一次;
  • ↑ 什么时候算翻历史:输入框为空、光标在最开头、或者正在翻历史且光标在第一行;有选中文字时不算;
  • ↓ 什么时候算翻历史:只在正在翻历史、并且光标在最后一行时;
  • 翻到最早一条就停住,翻过最新一条回到草稿。

其他命令:

  • bun run check:通过。
  • bun run test,Node 部分:2456 个,2446 通过、8 跳过、2 失败。
    • 失败的是 tests/extensions/plan-mode/result-rendering.test.ts 里的 a rich plan renders without throwing and keeps its content 和 expanded survives edge-content plans at extreme widths。
    • 这两个在没改过的 main 上本地也失败,上游 CI 是绿的,是我本机环境的问题。
  • bun run test:ui(Node 部分失败后 bun run test 不会继续,所以单独跑):109 个文件、1641 个用例。
    • 本机负载很高(load average 约 28)时完整跑了几次,每次都有一批和输入框无关的用例失败,失败的用例每次都不一样,基本都是 Test timed out in 5000ms。
    • 把最近一次失败的 14 个文件用 --maxWorkers=2 重跑,301/301 全部通过。
    • 所以这里没有一次完整、全绿的 UI 测试结果,只能说失败的都是负载导致的超时,和这次改动无关。
  • 没有在真实浏览器里手动试按键,也没测真实的中文输入法,只有上面的 jsdom 测试。

Impact

  • 用户可见: Web 输入框里按 ↑ / ↓ 可以翻自己发过的消息。光标在多行文字中间、斜杠命令菜单打开、按着修饰键、输入法拼字这几种情况下,按键行为和原来一样。
  • 模型可见的上下文和工具: 无。
  • 运行时和生命周期: 无。被截断的长消息在调出时会多发一两次 /api/session/item 请求,切换会话或卸载组件时会取消。
  • 持久化的配置和数据: 无。历史直接读当前会话,不额外存储。
  • 兼容性和风险: 输入框为空、或者光标在最开头时按 ↑,以前什么都不会发生,现在会调出上一条消息。这正是这个功能要的效果,和 TUI 一致。

🤖 Generated with Claude Code

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the TUI-parity rules and the "full text or nothing" handling for truncated prompts look right, and local check/test pass. The only red CI job is the shared openpi-web.e2e.ts unknown-admission test that is also failing on main, so it isn't caused by this PR.

Requested changes:

  1. web/ui/src/features/composer/Composer.tsx (recallPrompt): if (historyLoad.current) return true; runs before historyKeyApplies(...), so while a long prompt is loading every Up/Down press is swallowed, including ordinary caret movement on other lines. Please only consume the key when it would otherwise be a history key, or abort the pending load when the user moves the caret.
  2. If the full-text fetch fails (.catch(() => {})), please make sure the history state isn't left stuck. A small test for "load fails → next Up still works" would cover it.
  3. tests/web/composer-prompt-history.spec.ts: please keep only browser-level behaviors (keyboard interaction, draft restore, truncated-prompt fetch) and test the pure rules in prompt-history.ts once at unit level.

@6iKUN6

6iKUN6 commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — addressed in 6c6b68b:

  1. Up/Down swallowed during a long-prompt load — recallPrompt now checks historyKeyApplies(...) first and only waits on the pending load for a real history key. Ordinary caret movement is no longer blocked. New spec case "a pending full-text load does not swallow caret movement" fails against the previous code and passes now.
  2. Failed full-text fetch — the .finally already cleared historyLoad, so no code change was needed; added "a failed full-text load recalls nothing and the next Up tries again" to lock it in.
  3. Test layering — the pure rules in prompt-history.ts (which entries count, when Up/Down apply, step bounds) are now covered once in tests/web/prompt-history.test.ts (node:test). composer-prompt-history.spec.ts keeps only browser-level behavior: keyboard interaction, draft restore, modifiers/IME, slash menu, edit-and-send, truncated-prompt fetch, session switching.

Validation: bun run check passes; web/dist rebuilt. Node tests: 2446 pass / 8 skip / 2 fail (the same two local-only plan-mode rendering failures noted in the PR body). UI tests: some unrelated files timed out in the full local run; rerunning them individually they pass both with and without this change.

@tt-a1i

tt-a1i commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks for addressing all the review points in 6c6b68b — the changes look good. Since then main has moved (#905, #911, #914, #915, #916 landed) and this PR now conflicts. Could you rebase on current main? Once it's rebased and CI is green, I'll do the final review and merge.

6iKUN6 and others added 3 commits November 9, 2026 19:45
Pi's TUI editor recalls earlier prompts with Up and Down, but the Web
composer only used the arrows for the slash command menu. Add the same
behaviour.

Up recalls from an empty editor, from the very start of the text, or
while already browsing; Down walks back toward the newest prompt and then
restores the unsent draft. Any edit ends browsing. The slash command menu,
IME composition and modified arrows keep their existing behaviour.

History is the selected Session's loaded user prompts, using the same
setup-echo rules as turn navigation, so it survives a reload and is never
a second store of user input. A prompt the transcript only projects in
part is recalled from its complete native text, or not at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Up/Down during a pending full-text load were consumed before checking
whether they were history keys, which blocked ordinary caret movement.
Check the history rule first.

Tests: move the pure prompt-history rules to a node unit test, keep the
Composer spec to browser-level behavior, and cover "load fails, next Up
still works" and "pending load does not swallow caret movement".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tt-a1i
tt-a1i force-pushed the feat/web-composer-input-history branch from 6c6b68b to 715556f Compare November 9, 2026 11:48
@tt-a1i
tt-a1i merged commit 923ec86 into openpi-dev:main Nov 9, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants