Skip to content

Commit 673f13a

Browse files
edyedy
authored andcommitted
fix(tui): per-file polish round 3 — component/prompt and message
1 parent cdc1ad4 commit 673f13a

7 files changed

Lines changed: 79 additions & 116 deletions

File tree

MERGE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ fork 与上游改了同一处(常见于 TUI 视觉/UX、core 加固逻辑)
198198
| `packages/tui/src/util/markdown.ts` + `test/markdown-polish.test.ts` (LLM markdown 打磨 + splitProseAndCode) || fork 新增(LaTeX→Unicode、CJK 强调符修复、`splitProseAndCode` 分段供代码块面板化),保留;若上游引入同类能力对抗审计 |
199199
| 逐文件打磨轮1:`ui/{spinner,dialog-select,dialog-prompt,dialog-export-options,link,icon,glyphs}.ts(x)` + `{app,app-commands,clipboard,editor,logo,audio}.ts(x)` + `util/{collapse-tool-output,filetype,presentation}.ts` + `config/keybind.ts` || 保留 bug 修复:spinner 共享 RGBA 常量原地突变改克隆;dialog-select `selectedForeground` 惰值改响应式 + setTimeout 补 onCleanup;collapse-tool-output 负 hiddenCount 防护;app-commands KV 快照改响应式读 + heap snapshot undefined 提示 + isVersionGreater 多连字符预发布;clipboard GNU screen 用平 DCS 透传;editor $EDITOR 引号感知拆分;filetype 支持无扩展名/复合后缀;presentation 复用 logo.ts + 缺 sessionID 略去 Continue 行;删除死代码(icon.tsx 未用组件/映射表、glyphs 死导出、logo.marks、audio.stopVoice、Keybinds 壳、死 onCancel prop、app.tsx console.log)(上游若已做可取上游版本) |
200200
| 逐文件打磨轮2:`component/{dialog-workspace-create,dialog-console-org,dialog-mcp,dialog-retry-action,dialog-session-rename,dialog-workspace-unavailable,dialog-status,spinner,bg-pulse-render,error-component,todo-item,workspace-label}.ts(x)` || 保留 bug 修复:dialog-workspace-create 最近工作区按连接状态过滤 + 空 adapters 用 `<Show>` 包裹;dialog-console-org 切换失败弹 error toast;dialog-session-rename 重命名失败弹 error toast;dialog-workspace-unavailable 恢复成功后关闭对话框;dialog-retry-action `selectedForeground` 惰值改响应式;error-component 复制失败重置 Copied 态 + ✖ 改 `GLYPH.cross`;todo-item icon/color/attrs 改派生函数(响应式);删除死代码(dialog-mcp 未用 setRef、dialog-status 空 Props 类型、spinner SPINNER_FRAMES 别名、workspace-label 未用组件、bg-pulse-render 死导出与未用 cache:false 分支)(上游若已做可取上游版本) |
201+
| 逐文件打磨轮3:`component/prompt/{autocomplete,index,move,workspace}.tsx` + 删除 `component/prompt/creating-dots.ts`、`component/workspace-label.tsx` | 中 | 保留 bug 修复:autocomplete `<Index>` 行内 8 个 setup 期常量改派生函数(行复用时 label/图标/颜色冻结)+ files resource 源补 `store.visible`(弹窗打开且查询未变时不刷新);index shell/斜杠命令失败补 error toast(原静默丢输入)+ extmark 样式 ID 改访问器(主题切换后指向已销毁 SyntaxStyle)+ interrupt 双击计数器补 clearTimeout/onCleanup(陈旧定时器清零新计数)+ 内联 reduce 换用 `expandPastedTextPlaceholders` + `basename ?? "image"` 改 `\|\|`;workspace 死 notice 改为成功 warp 弹 success toast(原 showNotice 从未渲染);删除死代码(Autocomplete sessionID prop、AutocompleteOption.disabled、move creatingDots/pendingNew、workspace label memo、creating-dots.ts 与 workspace-label.tsx 整文件)(上游若已做可取上游版本) |
201202

202203
### 其他
203204

packages/tui/src/component/prompt/autocomplete.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ export type AutocompleteOption = {
6666
display: string
6767
value?: string
6868
aliases?: string[]
69-
disabled?: boolean
7069
description?: string
7170
isDirectory?: boolean
7271
onSelect?: () => void
@@ -75,14 +74,13 @@ export type AutocompleteOption = {
7574

7675
export function Autocomplete(props: {
7776
value: string
78-
sessionID?: string
7977
setPrompt: (input: (prompt: PromptInfo) => void) => void
8078
setExtmark: (partIndex: number, extmarkId: number) => void
8179
anchor: () => BoxRenderable
8280
input: () => TextareaRenderable
8381
ref: (ref: AutocompleteRef) => void
84-
fileStyleId: number
85-
agentStyleId: number
82+
fileStyleId: () => number
83+
agentStyleId: () => number
8684
promptPartTypeId: () => number
8785
}) {
8886
const editor = useEditorContext()
@@ -197,7 +195,8 @@ export function Autocomplete(props: {
197195
const extmarkStart = store.index
198196
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
199197

200-
const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined
198+
const styleId =
199+
part.type === "file" ? props.fileStyleId() : part.type === "agent" ? props.agentStyleId() : undefined
201200

202201
const extmarkId = input.extmarks.create({
203202
start: extmarkStart,
@@ -322,9 +321,9 @@ export function Autocomplete(props: {
322321
}
323322

324323
const [files] = createResource(
325-
() => ({ query: search(), location: location() }),
324+
() => ({ query: search(), location: location(), visible: store.visible }),
326325
async (input) => {
327-
if (!store.visible || store.visible === "/") return []
326+
if (!input.visible || input.visible === "/") return []
328327
if (referenceMatch()) return []
329328
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
330329

@@ -755,14 +754,15 @@ export function Autocomplete(props: {
755754
}
756755
>
757756
{(option, index) => {
758-
const isMcpCommand = option().display.includes(":mcp")
759-
const isSlashCommand = option().display.startsWith("/")
760-
const isAgent = option().display.startsWith("@") && !option().isDirectory && !option().path
761-
const isFile = option().path !== undefined
762-
const isDir = option().isDirectory
763-
const typeIcon = isMcpCommand ? "::" : ""
764-
const typeColor = isMcpCommand ? theme.accent : isAgent ? theme.secondary : isFile ? theme.info : theme.textMuted
765-
const label = isDir && !option().display.endsWith("/") ? option().display + "/" : option().display
757+
const isMcpCommand = () => option().display.includes(":mcp")
758+
const isSlashCommand = () => option().display.startsWith("/")
759+
const isAgent = () => option().display.startsWith("@") && !option().isDirectory && !option().path
760+
const isFile = () => option().path !== undefined
761+
const typeIcon = () => (isMcpCommand() ? "::" : "")
762+
const typeColor = () =>
763+
isMcpCommand() ? theme.accent : isAgent() ? theme.secondary : isFile() ? theme.info : theme.textMuted
764+
const label = () =>
765+
option().isDirectory && !option().display.endsWith("/") ? option().display + "/" : option().display
766766
return (
767767
<box
768768
paddingLeft={1}
@@ -783,17 +783,17 @@ export function Autocomplete(props: {
783783
}}
784784
onMouseUp={() => select()}
785785
>
786-
<Show when={typeIcon}>
787-
<text fg={typeColor} flexShrink={0}>
788-
{typeIcon}{" "}
786+
<Show when={typeIcon()}>
787+
<text fg={typeColor()} flexShrink={0}>
788+
{typeIcon()}{" "}
789789
</text>
790790
</Show>
791791
<text
792792
fg={index === store.selected ? selectedForeground(theme) : theme.text}
793-
attributes={isSlashCommand && !isMcpCommand ? TextAttributes.BOLD : undefined}
793+
attributes={isSlashCommand() && !isMcpCommand() ? TextAttributes.BOLD : undefined}
794794
flexShrink={0}
795795
>
796-
{label}
796+
{label()}
797797
</text>
798798
<Show when={option().description}>
799799
<text fg={index === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">

packages/tui/src/component/prompt/creating-dots.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

packages/tui/src/component/prompt/index.tsx

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,12 @@ export function Prompt(props: PromptProps) {
197197
setDismissedEditorSelectionKey(editorSelectionKey(editorContext()))
198198
editor.clearSelection()
199199
}
200-
const fileStyleId = syntax().getStyleId("extmark.file")!
201-
const agentStyleId = syntax().getStyleId("extmark.agent")!
202-
const pasteStyleId = syntax().getStyleId("extmark.paste")!
200+
const fileStyleId = () => syntax().getStyleId("extmark.file")!
201+
const agentStyleId = () => syntax().getStyleId("extmark.agent")!
202+
const pasteStyleId = () => syntax().getStyleId("extmark.paste")!
203203
let promptPartTypeId = 0
204+
let interruptTimer: ReturnType<typeof setTimeout> | undefined
205+
onCleanup(() => clearTimeout(interruptTimer))
204206
const event = useEvent()
205207

206208
onCleanup(
@@ -380,14 +382,16 @@ export function Prompt(props: PromptProps) {
380382

381383
setStore("interrupt", store.interrupt + 1)
382384

383-
setTimeout(() => {
385+
clearTimeout(interruptTimer)
386+
interruptTimer = setTimeout(() => {
384387
setStore("interrupt", 0)
385388
}, 5000)
386389

387390
if (store.interrupt >= 2) {
388391
void sdk.client.session.abort({
389392
sessionID: props.sessionID,
390393
})
394+
clearTimeout(interruptTimer)
391395
setStore("interrupt", 0)
392396
}
393397
dialog.clear()
@@ -402,12 +406,7 @@ export function Prompt(props: PromptProps) {
402406
dialog.clear()
403407

404408
// replace summarized text parts with the actual text
405-
const text = store.prompt.parts
406-
.filter((p) => p.type === "text")
407-
.reduce((acc, p) => {
408-
if (!p.source) return acc
409-
return acc.replace(p.source.text.value, p.text)
410-
}, store.prompt.input)
409+
const text = expandPastedTextPlaceholders(store.prompt.input, store.prompt.parts)
411410

412411
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
413412

@@ -642,17 +641,17 @@ export function Prompt(props: PromptProps) {
642641
start = part.source.text.start
643642
end = part.source.text.end
644643
virtualText = part.source.text.value
645-
styleId = fileStyleId
644+
styleId = fileStyleId()
646645
} else if (part.type === "agent" && part.source) {
647646
start = part.source.start
648647
end = part.source.end
649648
virtualText = part.source.value
650-
styleId = agentStyleId
649+
styleId = agentStyleId()
651650
} else if (part.type === "text" && part.source?.text) {
652651
start = part.source.text.start
653652
end = part.source.text.end
654653
virtualText = part.source.text.value
655-
styleId = pasteStyleId
654+
styleId = pasteStyleId()
656655
}
657656

658657
if (virtualText) {
@@ -918,8 +917,6 @@ export function Prompt(props: PromptProps) {
918917
}
919918

920919
async function submitInner() {
921-
workspace.clearNotice()
922-
923920
// IME: double-defer may fire before onContentChange flushes the last
924921
// composed character (e.g. Korean hangul) to the store, so read
925922
// plainText directly and sync before any downstream reads.
@@ -1031,15 +1028,26 @@ export function Prompt(props: PromptProps) {
10311028

10321029
if (store.mode === "shell") {
10331030
move.startSubmit()
1034-
void sdk.client.session.shell({
1035-
sessionID,
1036-
agent: agent.name,
1037-
model: {
1038-
providerID: selectedModel.providerID,
1039-
modelID: selectedModel.modelID,
1040-
},
1041-
command: inputText,
1042-
})
1031+
sdk.client.session
1032+
.shell(
1033+
{
1034+
sessionID,
1035+
agent: agent.name,
1036+
model: {
1037+
providerID: selectedModel.providerID,
1038+
modelID: selectedModel.modelID,
1039+
},
1040+
command: inputText,
1041+
},
1042+
{ throwOnError: true },
1043+
)
1044+
.catch((error) => {
1045+
toast.show({
1046+
title: "Failed to run shell command",
1047+
message: errorMessage(error),
1048+
variant: "error",
1049+
})
1050+
})
10431051
setStore("mode", "normal")
10441052
} else if (
10451053
inputText.startsWith("/") &&
@@ -1053,15 +1061,26 @@ export function Prompt(props: PromptProps) {
10531061
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
10541062
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
10551063

1056-
void sdk.client.session.command({
1057-
sessionID,
1058-
command: command.slice(1),
1059-
arguments: args,
1060-
agent: agent.name,
1061-
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
1062-
variant,
1063-
parts: nonTextParts.filter((x) => x.type === "file"),
1064-
})
1064+
sdk.client.session
1065+
.command(
1066+
{
1067+
sessionID,
1068+
command: command.slice(1),
1069+
arguments: args,
1070+
agent: agent.name,
1071+
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
1072+
variant,
1073+
parts: nonTextParts.filter((x) => x.type === "file"),
1074+
},
1075+
{ throwOnError: true },
1076+
)
1077+
.catch((error) => {
1078+
toast.show({
1079+
title: "Failed to run command",
1080+
message: errorMessage(error),
1081+
variant: "error",
1082+
})
1083+
})
10651084
} else {
10661085
move.startSubmit()
10671086
sdk.client.session
@@ -1130,7 +1149,7 @@ export function Prompt(props: PromptProps) {
11301149
start: extmarkStart,
11311150
end: extmarkEnd,
11321151
virtual: true,
1133-
styleId: pasteStyleId,
1152+
styleId: pasteStyleId(),
11341153
typeId: promptPartTypeId,
11351154
})
11361155

@@ -1162,7 +1181,7 @@ export function Prompt(props: PromptProps) {
11621181
const attachment = await readLocalAttachment(filepath)
11631182
const filename = path.basename(filepath)
11641183
if (attachment?.type === "text") {
1165-
pasteText(attachment.content, `[SVG: ${filename ?? "image"}]`)
1184+
pasteText(attachment.content, `[SVG: ${filename || "image"}]`)
11661185
return
11671186
}
11681187
if (attachment?.type === "binary") {
@@ -1213,7 +1232,7 @@ export function Prompt(props: PromptProps) {
12131232
start: extmarkStart,
12141233
end: extmarkEnd,
12151234
virtual: true,
1216-
styleId: pasteStyleId,
1235+
styleId: pasteStyleId(),
12171236
typeId: promptPartTypeId,
12181237
})
12191238

@@ -1483,7 +1502,6 @@ export function Prompt(props: PromptProps) {
14831502
</Show>
14841503
</box>
14851504
<Autocomplete
1486-
sessionID={props.sessionID}
14871505
ref={(r) => {
14881506
setAuto(() => r)
14891507
}}

packages/tui/src/component/prompt/move.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createMemo, createSignal } from "solid-js"
22
import path from "path"
3-
import { useCreatingDots } from "./creating-dots"
43
import { useTuiPaths } from "../../context/runtime"
54
import { errorMessage } from "../../util/error"
65
import { useDialog } from "../../ui/dialog"
@@ -25,7 +24,6 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
2524
const project = useProject()
2625
const paths = useTuiPaths()
2726
const [creating, setCreating] = createSignal(false)
28-
const creatingDots = useCreatingDots(creating)
2927
const [progress, setProgress] = createSignal<string>()
3028

3129
async function create(context?: string) {
@@ -162,7 +160,6 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
162160
}
163161

164162
const pending = createMemo(() => Boolean(homeDestination?.destination()))
165-
const pendingNew = createMemo(() => homeDestination?.destination()?.type === "new")
166163

167164
async function getDirectory(context?: string) {
168165
const value = homeDestination?.destination()
@@ -185,12 +182,10 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
185182

186183
return {
187184
creating,
188-
creatingDots,
189185
finishSubmit,
190186
getDirectory,
191187
open,
192188
pending,
193-
pendingNew,
194189
progress,
195190
startSubmit,
196191
}

0 commit comments

Comments
 (0)