Skip to content

Commit fbad8d4

Browse files
author
SqlRush
committed
Wire clear result into REPL screen
1 parent 880b0b7 commit fbad8d4

8 files changed

Lines changed: 114 additions & 4 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,9 @@ M8 补充:project legacy `.claude/commands/**/*.md` 和 user legacy `${CLAUDE_
245245

246246
M8 补充:现有 Go 内置 slash command metadata 继续贴近官方源快照,补齐 `config`/`resume`/`clear` 的 aliases(`settings``continue``reset``new`),以及 `mcp`/`resume`/`model` 的 argument hint、`mcp`/`status`/`model` 的 immediate 标记和部分官方描述;大量内置 command 的真实 local/local-jsx UI 执行仍未完成。
247247

248-
M8 补充:slash command 现在有基础 local command result 抽象,`/clear` 不再落入 unsupported 分支,会生成 local text result、保留 command metadata message,并且不会请求模型;完整 REPL conversation reset、local command text/compact/skip 全语义、`/cost`/`/status`/`/compact` 和 local-jsx UI 执行仍未完成。
248+
M8 补充:slash command 现在有基础 local command result 抽象,`/clear` 不再落入 unsupported 分支,会生成 local text result、保留 command metadata message,并且不会请求模型;REPL screen 已提供 clear-result 消费路径,可清空 transcript viewport/selection/reverse-search 并按当前 draft 重算 paste/image 引用编号;完整 local command text/compact/skip 全语义、`/cost`/`/status`/`/compact` 和 local-jsx UI 执行仍未完成。
249249

250-
M8/M7 补充:`/clear` local command 现在返回专用 clear result,并在 conversation `Result.Cleared` 中暴露结构化清空信号;runner 仍保留 command metadata transcript 且不请求模型,完整 TUI/REPL 历史重置接线仍需由交互主循环消费该信号完成
250+
M8/M7 补充:`/clear` local command 现在返回专用 clear result,并在 conversation `Result.Cleared` 中暴露结构化清空信号;`REPLScreen.ClearConversation` 和交互脚本 `conversationCleared` alias 可消费该信号,清空屏幕会话历史但保留 prompt 输入历史,runner 仍保留 command metadata transcript 且不请求模型。
251251

252252
M8/CLI 补充:`--print --output-format json|stream-json` 现在会为 `/clear` 这类空文本本地结果输出 final result envelope,并用 `cleared: true` 暴露清空信号;普通空文本结果仍保持不输出,完整 SDK/control protocol clear event parity 仍需后续补齐。
253253

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ test/parity/ # golden tests against TS/official behavior
312312
- 本轮补充:skill frontmatter 标量兼容继续补齐,`allowed_tools`/`argument_hint`/`disable_model_invocation`/`user_invocable`/`when-to-use` 等相邻字段会映射到 canonical command metadata;`model: inherit` 不再误触发模型覆盖,`context: fork``agent``effort` 会保留在 command contract 中,为后续 forked skill/agent 执行接线提供 metadata;当 policy 锁定 `agents` surface 时,非可信来源 prompt command 的这些 agent metadata 会在 registry 层清除,plugin/bundled/admin 来源保留。
313313
- 本轮补充:project legacy `.claude/commands/**/*.md` 现在会加载为 `commands_DEPRECATED` prompt command,覆盖普通 markdown 命名空间、目录式 `SKILL.md` 命名空间、frontmatter metadata、SkillTool 可见性过滤和 prompt expansion;目录式 legacy command 保留 base directory 前缀和 `${CLAUDE_SKILL_DIR}` 替换。完整 user/managed commands、plugin commands、local/local-jsx 执行仍未宣称完成。
314314
- 本轮补充:现有 Go 内置 slash command metadata 继续贴近官方源快照,补齐 `config`/`resume`/`clear` 的 aliases(`settings``continue``reset``new`),以及 `mcp`/`resume`/`model` 的 argument hint、`mcp`/`status`/`model` 的 immediate 标记和部分官方描述;大量内置 command 的真实 local/local-jsx UI 执行仍未宣称完成。
315-
- 本轮补充:slash command 现在有基础 local command result 抽象,`/clear` 不再落入 unsupported 分支,会生成 local text result、保留 command metadata message,并且不会请求模型;完整 REPL conversation reset、local command text/compact/skip 全语义、`/cost`/`/status`/`/compact` 和 local-jsx UI 执行仍未宣称完成。
315+
- 本轮补充:slash command 现在有基础 local command result 抽象,`/clear` 不再落入 unsupported 分支,会生成 local text result、保留 command metadata message,并且不会请求模型;REPL screen 已提供 clear-result 消费路径,可清空 transcript viewport/selection/reverse-search 并按当前 draft 重算 paste/image 引用编号;完整 local command text/compact/skip 全语义、`/cost`/`/status`/`/compact` 和 local-jsx UI 执行仍未宣称完成。
316316
- 本轮补充:`/cost status``/cost current``/cost usage` 现在复用 `/cost` totals no-query 本地路径,未知 cost 子命令仍保持显式未实现提示,`/cost breakdown`/`show` 继续输出逐消息明细。
317317
- 本轮补充:`/memory files``/memory ls` 现在复用 `/memory list` 的 no-query 本地记忆文件列表路径,`/memory status` 仍保持会话/相关记忆概览。
318318
- 本轮补充:`/status <section>` 现在复用 `/status show <section>` 的 section alias normalization,`/status integrations``/status diagnostics` 等直接调用不会再落入 unsupported subcommand 分支。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Anthropic API 和 conversation:
7676
- `internal/commands` now has a metadata registry that merges command sources in the official order, loads project skill commands from the working directory, deduplicates dynamic skills, resolves commands by canonical name/display name/alias, filters visible commands, exposes SkillTool/slash-skill command subsets, and applies the bridge-safe predicate for prompt/local/local-jsx commands.
7777
- Built-in slash command metadata now preserves official aliases for the currently declared Go builtins such as `settings`, `continue`, `reset`, and `new`, plus argument hints and immediate markers for `mcp`, `resume`, `status`, and `model`.
7878
- The command registry now retains local skill prompt templates and can expand prompt commands into meta user messages, including `$ARGUMENTS`, indexed/shorthand arguments, frontmatter named arguments, `${CLAUDE_SESSION_ID}` substitution, and non-MCP `${CLAUDE_SKILL_DIR}` substitution.
79-
- Slash command execution now has a basic local command result abstraction, and `/clear` follows a no-query local text-result path instead of returning the generic unsupported local-command stderr; full local command result handling and REPL state reset remain incomplete.
79+
- Slash command execution now has a basic local command result abstraction, `/clear` follows a no-query local text-result path, and the REPL screen can consume the cleared result by resetting transcript viewport/selection/reverse-search while preserving prompt input history; broader local command result handling remains incomplete.
8080
- `/status <section>` now shares the same section-alias normalization as `/status show <section>`, so direct invocations such as `/status integrations` and `/status diagnostics` take the no-query local status path instead of falling into the unsupported-subcommand branch.
8181
- `/cost status`, `/cost current`, and `/cost usage` now share the no-query totals path with `/cost`, while `/cost breakdown`/`show` continue to return per-message usage details and unknown subcommands remain explicit.
8282
- `/summary`, `/release-notes`, and `/files` are now registered as bridge-safe built-in local commands and take no-query local result paths: `/summary` reports deterministic session/history counts and previews, `/release-notes` reports the bundled release-notes availability state, and `/files` lists first-level workspace entries without reading file contents.

internal/tui/input.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,28 @@ func (p *PromptState) SeedNextPastedIDFromMessages(messages []Message) {
943943
p.NextPastedID = next
944944
}
945945

946+
func (p *PromptState) resetPastedIDFromDraft() {
947+
if !p.UsePasteReferences {
948+
return
949+
}
950+
next := 1
951+
for id, content := range p.PastedContents {
952+
contentID := id
953+
if content.ID > 0 {
954+
contentID = content.ID
955+
}
956+
if contentID >= next {
957+
next = contentID + 1
958+
}
959+
}
960+
for _, ref := range session.ParseReferences(p.Text) {
961+
if ref.ID >= next {
962+
next = ref.ID + 1
963+
}
964+
}
965+
p.NextPastedID = next
966+
}
967+
946968
func (p *PromptState) SetPasteReferenceRows(rows int) {
947969
p.MaxInlinePasteLines = maxInlinePasteLines(rows)
948970
}

internal/tui/screen.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ func (s *REPLScreen) AppendMessage(message Message) {
145145
s.Viewport.ScrollToBottom()
146146
}
147147

148+
func (s *REPLScreen) ClearConversation() {
149+
s.Messages = nil
150+
s.SelectedViewportLine = -1
151+
s.ReverseSearch = ReverseSearchState{}
152+
s.Prompt.resetPastedIDFromDraft()
153+
s.rebuildViewport()
154+
s.Viewport.ScrollToTop()
155+
}
156+
148157
func (s *REPLScreen) ApplyKey(key Key) ScreenEvent {
149158
switch key.Type {
150159
case KeyFocusIn:

internal/tui/script.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ScriptStep struct {
2828
CancelAllTasks bool
2929
CancelTasksDetail string
3030
OpenTasksDialog bool
31+
ClearConversation bool
3132
ResizeWidth int
3233
ResizeHeight int
3334
SnapshotName string
@@ -223,6 +224,9 @@ func runInteractionScriptChecked(screen *REPLScreen, steps []ScriptStep, runtime
223224
}
224225
screen.Keymap = keymap
225226
}
227+
if step.ClearConversation {
228+
screen.ClearConversation()
229+
}
226230
if step.ResizeWidth > 0 {
227231
width := step.ResizeWidth
228232
height := screen.Height

internal/tui/script_aliases.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,12 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
712712
OpenTasksCamel *json.RawMessage `json:"openTasks"`
713713
ShowTasks *json.RawMessage `json:"show_tasks"`
714714
ShowTasksCamel *json.RawMessage `json:"showTasks"`
715+
ClearConversation *json.RawMessage `json:"clear_conversation"`
716+
ClearConversationCamel *json.RawMessage `json:"clearConversation"`
717+
ConversationCleared *json.RawMessage `json:"conversation_cleared"`
718+
ConversationClearedCamel *json.RawMessage `json:"conversationCleared"`
719+
ApplyClearResult *json.RawMessage `json:"apply_clear_result"`
720+
ApplyClearResultCamel *json.RawMessage `json:"applyClearResult"`
715721
ResizeWidth *json.RawMessage `json:"resize_width"`
716722
ResizeWidthCamel *json.RawMessage `json:"resizeWidth"`
717723
ResizeHeight *json.RawMessage `json:"resize_height"`
@@ -1009,6 +1015,9 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
10091015
if value, ok := scriptRuntimeMutationBoolField(fieldMap, "OpenTasksDialog", "open_tasks_dialog", "openTasksDialog", "OpenTasks", "open_tasks", "openTasks", "ShowTasks", "show_tasks", "showTasks"); ok {
10101016
step.OpenTasksDialog = value
10111017
}
1018+
if value, ok := scriptRuntimeMutationBoolField(fieldMap, "ClearConversation", "clear_conversation", "clearConversation", "ConversationCleared", "conversation_cleared", "conversationCleared", "ApplyClearResult", "apply_clear_result", "applyClearResult"); ok {
1019+
step.ClearConversation = value
1020+
}
10121021
if step.ResizeWidth <= 0 {
10131022
if width, ok := scriptNamedIntField(rawFieldMap,
10141023
[]string{"ResizeWidth", "resize_width", "resizeWidth"},
@@ -1782,6 +1791,12 @@ func scriptStepJSONHasDirectFields(fields map[string]json.RawMessage) bool {
17821791
"task_status",
17831792
"taskStatus",
17841793
"dialog",
1794+
"clear_conversation",
1795+
"clearConversation",
1796+
"conversation_cleared",
1797+
"conversationCleared",
1798+
"apply_clear_result",
1799+
"applyClearResult",
17851800
"expect_event",
17861801
"expectEvent",
17871802
"expect_events",
@@ -1902,6 +1917,8 @@ func applyScriptStepActionAlias(step *ScriptStep, fields map[string]json.RawMess
19021917
}
19031918
case "opentasks", "open-tasks", "opentasksdialog", "open-tasks-dialog", "showtasks", "show-tasks":
19041919
step.OpenTasksDialog = scriptActionBoolField(fields, true)
1920+
case "clearconversation", "clear-conversation", "conversationcleared", "conversation-cleared", "applyclearresult", "apply-clear-result", "clearresult", "clear-result":
1921+
step.ClearConversation = scriptActionBoolField(fields, true)
19051922
case "cancelactivedialog", "cancel-active-dialog", "canceldialog", "cancel-dialog", "closedialog", "close-dialog":
19061923
step.CancelActiveDialog = scriptActionBoolField(fields, true)
19071924
case "cancelpermission", "cancel-permission":

internal/tui/tui_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,38 @@ func TestREPLScreenSeedsNextPastedIDFromMessages(t *testing.T) {
595595
}
596596
}
597597

598+
func TestREPLScreenClearConversationResetsTranscriptState(t *testing.T) {
599+
screen := NewREPLScreen(40, 6, []string{"previous prompt"})
600+
screen.SetMessages([]Message{
601+
{Role: RoleUser, Text: "old [Image #9]"},
602+
{Role: RoleAssistant, Text: "old answer"},
603+
{Role: RoleUser, Text: "older [Pasted text #12]"},
604+
})
605+
screen.SelectedViewportLine = 1
606+
screen.OpenReverseSearch("previous")
607+
screen.Prompt.Text = "draft [Image #4]"
608+
screen.Prompt.PastedContents = map[int]session.PastedContent{
609+
4: {ID: 4, Type: session.PastedContentImage, Filename: "draft.png"},
610+
}
611+
screen.Prompt.NextPastedID = 13
612+
613+
screen.ClearConversation()
614+
615+
if len(screen.Messages) != 0 || len(screen.Viewport.Lines) != 0 || screen.Viewport.Offset != 0 {
616+
t.Fatalf("screen transcript not cleared: messages=%#v viewport=%#v", screen.Messages, screen.Viewport)
617+
}
618+
if screen.SelectedViewportLine != -1 || screen.ReverseSearch.Active {
619+
t.Fatalf("selection/search state = line %d reverse=%#v", screen.SelectedViewportLine, screen.ReverseSearch)
620+
}
621+
if screen.Prompt.Text != "draft [Image #4]" || screen.Prompt.NextPastedID != 5 || screen.Prompt.historyLength() != 1 {
622+
t.Fatalf("prompt after clear = %#v", screen.Prompt)
623+
}
624+
screen.ApplyKey(ParseKey("\x1b]1337;File=name=next.png;type=image/png;inline=1:BBBB\a"))
625+
if !strings.Contains(screen.Prompt.Text, "[Image #5]") || screen.Prompt.NextPastedID != 6 {
626+
t.Fatalf("prompt pasted id after clear = %#v", screen.Prompt)
627+
}
628+
}
629+
598630
func TestPromptImageHintWritesImageCacheWhenEnabled(t *testing.T) {
599631
dir := t.TempDir()
600632
t.Setenv("CLAUDE_CONFIG_DIR", dir)
@@ -11199,6 +11231,32 @@ func TestRunInteractionScriptChecksEventSequences(t *testing.T) {
1119911231
}
1120011232
}
1120111233

11234+
func TestRunInteractionScriptAppliesConversationClearedAlias(t *testing.T) {
11235+
steps, err := ParseInteractionScript([]byte(`[
11236+
{"message": {"role": "user", "text": "old question"}},
11237+
{"message": {"role": "assistant", "text": "old answer"}},
11238+
{
11239+
"conversationCleared": true,
11240+
"expectSnapshotNotContains": ["old question", "old answer"],
11241+
"expectViewport": {"visibleNotContains": ["old question", "old answer"]},
11242+
"expectNoEvent": true
11243+
}
11244+
]`))
11245+
if err != nil {
11246+
t.Fatal(err)
11247+
}
11248+
if !steps[2].ClearConversation {
11249+
t.Fatalf("clear alias not parsed: %#v", steps[2])
11250+
}
11251+
screen := NewREPLScreen(40, 8, nil)
11252+
if _, err := RunInteractionScriptChecked(&screen, steps); err != nil {
11253+
t.Fatal(err)
11254+
}
11255+
if len(screen.Messages) != 0 || len(screen.Viewport.Lines) != 0 {
11256+
t.Fatalf("screen not cleared: messages=%#v viewport=%#v", screen.Messages, screen.Viewport)
11257+
}
11258+
}
11259+
1120211260
func TestRunInteractionScriptChecksJSONEventCounts(t *testing.T) {
1120311261
steps, err := ParseInteractionScript([]byte(`[
1120411262
{

0 commit comments

Comments
 (0)