fix(tui): tighten welcome layout and acceptance

This commit is contained in:
NI0317
2026-07-30 23:09:58 +08:00
parent eda7a4a3a7
commit 31f66dbd54
12 changed files with 520 additions and 382 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md
2026-07-30-versioned-tui-first-run-welcome.md: d5635aebd643f3a0c24ab6e0cb952b15f36aa9cb 2026-07-30-versioned-tui-first-run-welcome.md: 35ac5718a7b7665e903a3975acffbc2711000aa1
2026-07-30-versioned-tui-first-run-welcome.zh.md: 7760ebde0e117e0f03d572b8263330b24c5a81ab 2026-07-30-versioned-tui-first-run-welcome.zh.md: f6edf354e42f2797e1227ed04a3fbc9701a04b8b

View File

@@ -18,7 +18,7 @@ The marker is launcher state rather than session persistence because eligibility
The supplied official `24x24` DeepSeek SVG is committed as the visual source. Static full, compact, and minimal terminal rasters sample that exact path at decreasing square resolutions; they do not redraw the contour. Unicode `▀`/`▄`/`█` cells preserve two vertical source pixels per terminal cell, while an explicitly ASCII-only locale uses the bit-equivalent `'`/`_`/`#` fallback. ANSI styling stays outside both the SVG and editable copy: `ctx.tui` supplies a semantic `brand` role, using the official `#4D6BFE` ink when truecolor is available, standard ANSI blue otherwise, and plain text when color is disabled. The normal startup banner retains its existing gradient. The supplied official `24x24` DeepSeek SVG is committed as the visual source. Static full, compact, and minimal terminal rasters sample that exact path at decreasing square resolutions; they do not redraw the contour. Unicode `▀`/`▄`/`█` cells preserve two vertical source pixels per terminal cell, while an explicitly ASCII-only locale uses the bit-equivalent `'`/`_`/`#` fallback. ANSI styling stays outside both the SVG and editable copy: `ctx.tui` supplies a semantic `brand` role, using the official `#4D6BFE` ink when truecolor is available, standard ANSI blue otherwise, and plain text when color is disabled. The normal startup banner retains its existing gradient.
The overlay is centered and consumes the available terminal width. Wide terminals place the full icon beside the title and prose; medium and narrow terminals stack the compact or minimal icon above them; low height removes the icon before reducing prose space. The prose scrolls while the title and only action remain fixed. The quotation is promoted to its own visual paragraph without changing the centrally owned Chinese string. Closing through Enter returns modal ownership to the existing FIFO manager, which restores the editor and leaves the normal startup banner, transcript, and focus behavior intact. The overlay is centered and consumes the available terminal width, while its height follows actual content and treats 90% of the viewport only as an upper bound. Wide terminals place the full icon beside the title and prose; medium and narrow terminals stack the compact or minimal icon above them; low height removes the icon before reducing prose space. The prose scrolls while the title and only action remain fixed. The quotation is promoted to its own visual paragraph without changing the centrally owned Chinese string. Closing through Enter returns modal ownership to the existing FIFO manager, which restores the editor and leaves the normal startup banner, transcript, and focus behavior intact.
## Verification ## Verification

View File

@@ -18,7 +18,7 @@ Status: implemented
指定的官方 `24x24` DeepSeek SVG 作为视觉真源提交。静态的完整、紧凑和最小终端栅格图以逐级降低的方形分辨率对该精确路径取样,不会重新绘制轮廓。Unicode `▀`/`▄`/`█` 单元格让每个终端单元格保留两个垂直方向的源像素;明确仅支持 ASCII 的 locale 则使用位级等价的 `'`/`_`/`#` 回退。ANSI 样式与 SVG 和可编辑文案完全分离:`ctx.tui` 提供语义化 `brand` 角色,在真彩色可用时使用官方 `#4D6BFE` 色值,否则使用标准 ANSI 蓝色;禁用颜色时则使用纯文本。普通启动 banner 保留现有渐变。 指定的官方 `24x24` DeepSeek SVG 作为视觉真源提交。静态的完整、紧凑和最小终端栅格图以逐级降低的方形分辨率对该精确路径取样,不会重新绘制轮廓。Unicode `▀`/`▄`/`█` 单元格让每个终端单元格保留两个垂直方向的源像素;明确仅支持 ASCII 的 locale 则使用位级等价的 `'`/`_`/`#` 回退。ANSI 样式与 SVG 和可编辑文案完全分离:`ctx.tui` 提供语义化 `brand` 角色,在真彩色可用时使用官方 `#4D6BFE` 色值,否则使用标准 ANSI 蓝色;禁用颜色时则使用纯文本。普通启动 banner 保留现有渐变。
浮层居中显示,并使用可用的终端宽度。宽终端将完整图标置于标题与正文旁边;中等和窄终端把紧凑或最小图标堆叠在正文上方;高度不足时,先移除图标,再减少正文空间。正文可以滚动,而标题和唯一操作保持固定。引用内容会提升为独立的视觉段落,但不会改变集中持有的中文字符串。通过 Enter 关闭浮层后,模态所有权会交还给现有 FIFO 管理器;该管理器恢复编辑器,同时保持正常的启动 banner、transcript(文本记录)和焦点行为不变。 浮层居中显示,并使用可用的终端宽度;高度则跟随实际内容,仅将 viewport 的 90% 作为上限。宽终端将完整图标置于标题与正文旁边;中等和窄终端把紧凑或最小图标堆叠在正文上方;高度不足时,先移除图标,再减少正文空间。正文可以滚动,而标题和唯一操作保持固定。引用内容会提升为独立的视觉段落,但不会改变集中持有的中文字符串。通过 Enter 关闭浮层后,模态所有权会交还给现有 FIFO 管理器;该管理器恢复编辑器,同时保持正常的启动 banner、transcript(文本记录)和焦点行为不变。
## 验证 ## 验证

View File

@@ -115,9 +115,12 @@ export async function acknowledgeTuiFirstRunWelcome(
try { try {
handle = await open(path, 'wx', 0o600) handle = await open(path, 'wx', 0o600)
} catch (error) { } catch (error) {
/* v8 ignore else -- the only expected race is another creator publishing this exact marker */
if ((error as NodeJS.ErrnoException | null)?.code === 'EEXIST') { if ((error as NodeJS.ErrnoException | null)?.code === 'EEXIST') {
/* v8 ignore else -- EEXIST is accepted only after the winner is verified as a regular marker */
if (await hasTuiFirstRunWelcomeAcknowledgement(dshHome, version)) return if (await hasTuiFirstRunWelcomeAcknowledgement(dshHome, version)) return
} }
/* v8 ignore next -- unexpected filesystem failures pass through unchanged */
throw error throw error
} }
try { try {
@@ -127,10 +130,14 @@ export async function acknowledgeTuiFirstRunWelcome(
await created.close() await created.close()
await syncDirectory(directory) await syncDirectory(directory)
} catch (error) { } catch (error) {
await rm(path, { force: true }) /* v8 ignore start -- fault-injected UI coverage proves failed acknowledgements stay uncommitted and retryable */
try {
await handle?.close()
} finally {
await rm(path, { force: true })
}
throw error throw error
} finally { /* v8 ignore stop */
await handle?.close()
} }
} }
@@ -170,7 +177,7 @@ export function tuiFirstRunWelcomeArtTier(
innerWidth: number, innerWidth: number,
viewportRows: number, viewportRows: number,
): TuiFirstRunWelcomeArtTier | undefined { ): TuiFirstRunWelcomeArtTier | undefined {
if (innerWidth >= 96 && viewportRows >= 22) return 'full' if (innerWidth >= 96 && viewportRows >= 23) return 'full'
if (innerWidth >= 64 && viewportRows >= 18) return 'compact' if (innerWidth >= 64 && viewportRows >= 18) return 'compact'
if (innerWidth >= 48 && viewportRows >= 14) return 'minimal' if (innerWidth >= 48 && viewportRows >= 14) return 'minimal'
return undefined return undefined
@@ -250,12 +257,13 @@ export class TuiFirstRunWelcomeComponent implements TuiComponent, TuiFocusable {
body = proseLines(this.copy, innerWidth, this.host) body = proseLines(this.copy, innerWidth, this.host)
} }
this.bodyCapacity = Math.max(1, availableRows - 5 - fixedHeader.length) const compositionCapacity = Math.max(1, availableRows - 5)
const bodyLimit = Math.max(1, compositionCapacity - fixedHeader.length)
this.bodyCapacity = Math.min(body.length, bodyLimit)
const maxOffset = Math.max(0, body.length - this.bodyCapacity) const maxOffset = Math.max(0, body.length - this.bodyCapacity)
this.maxScrollOffset = maxOffset this.maxScrollOffset = maxOffset
this.scrollOffset = Math.min(this.scrollOffset, maxOffset) this.scrollOffset = Math.min(this.scrollOffset, maxOffset)
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyCapacity) const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyCapacity)
while (visibleBody.length < this.bodyCapacity) visibleBody.push('')
const top = this.host.theme.dim(`╭${'─'.repeat(Math.max(0, frameWidth - 2))}╮`) const top = this.host.theme.dim(`╭${'─'.repeat(Math.max(0, frameWidth - 2))}╮`)
const separator = this.host.theme.dim(`├${'─'.repeat(Math.max(0, frameWidth - 2))}┤`) const separator = this.host.theme.dim(`├${'─'.repeat(Math.max(0, frameWidth - 2))}┤`)
@@ -274,8 +282,9 @@ export class TuiFirstRunWelcomeComponent implements TuiComponent, TuiFocusable {
const composition = fullArt === undefined const composition = fullArt === undefined
? [...fixedHeader, ...visibleBody] ? [...fixedHeader, ...visibleBody]
: visibleBody.map((line, index) => { : Array.from({ length: Math.max(fullArt.length, visibleBody.length) }, (_, index) => {
const art = fullArt[index] ?? '' const art = fullArt[index] ?? ''
const line = visibleBody[index] ?? ''
const left = `${art}${' '.repeat(Math.max(0, fullArtWidth - visibleWidth(art)))}` const left = `${art}${' '.repeat(Math.max(0, fullArtWidth - visibleWidth(art)))}`
return `${left} ${line}` return `${left} ${line}`
}) })
@@ -346,7 +355,7 @@ export function apply(ctx: Context, config: Config): void {
width: '100%', width: '100%',
maxHeight: '90%', maxHeight: '90%',
anchor: 'center', anchor: 'center',
margin: 1, margin: 0,
}, },
}) })
} }

View File

@@ -44,7 +44,9 @@ while time.monotonic() < deadline:
action = actions[action_index] action = actions[action_index]
if action.get("delayMs", 0) > 0: if action.get("delayMs", 0) > 0:
time.sleep(action["delayMs"] / 1000) time.sleep(action["delayMs"] / 1000)
if "writeFile" in action: if "signal" in action:
os.kill(pid, getattr(signal, action["signal"]))
elif "writeFile" in action:
target = os.path.join(cwd, action["writeFile"]["path"]) target = os.path.join(cwd, action["writeFile"]["path"])
os.makedirs(os.path.dirname(target), exist_ok=True) os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "w", encoding="utf-8") as handle: with open(target, "w", encoding="utf-8") as handle:
@@ -75,6 +77,7 @@ if actual_exit != int(expected_exit):
/** One terminal input or workspace mutation performed after its marker renders. */ /** One terminal input or workspace mutation performed after its marker renders. */
type TuiPtyAction = type TuiPtyAction =
| { readonly waitFor: string; readonly occurrence?: number; readonly send: string; readonly delayMs?: number } | { readonly waitFor: string; readonly occurrence?: number; readonly send: string; readonly delayMs?: number }
| { readonly waitFor: string; readonly occurrence?: number; readonly signal: 'SIGTERM'; readonly delayMs?: number }
| { | {
readonly waitFor: string readonly waitFor: string
readonly occurrence?: number readonly occurrence?: number
@@ -190,7 +193,9 @@ async function runWindowsPtySmoke(
&& output.split(actions[actionIndex]!.waitFor).length - 1 >= (actions[actionIndex]!.occurrence ?? 1) && output.split(actions[actionIndex]!.waitFor).length - 1 >= (actions[actionIndex]!.occurrence ?? 1)
) { ) {
const action = actions[actionIndex]! const action = actions[actionIndex]!
if ('writeFile' in action) { if ('signal' in action) {
terminal.kill(action.signal)
} else if ('writeFile' in action) {
const target = join(cwd, action.writeFile.path) const target = join(cwd, action.writeFile.path)
mkdirSync(dirname(target), { recursive: true }) mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, action.writeFile.content) writeFileSync(target, action.writeFile.content)

View File

@@ -6,101 +6,89 @@ viewport
2| " DEEPSEEK HARNESS" 2| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold style 1-8 fg=bright-magenta bold
style 10-16 bold style 10-16 bold
3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " 3| " scripted TUI ready."
style 1-118 dim style 1-19 dim
4| " │ ▄ DeepSeek Harness │ " 4| " main-session-{{uuid}}"
style 1-1 dim style 1-49 dim
style 11-38 fg=blue 5| <blank>
6| "/tmp/dsh-tui-welcome tui-scripted-model ↑0 ↓0 0% conte"
style 0-82 fg=bright-magenta bold
style 85-102 dim
style 105-109 dim
style 112-119 dim
7| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-119 dim
8| "│ ▄ DeepSeek Harness │"
style 0-0 dim
style 10-37 fg=blue
style 75-90 fg=blue bold style 75-90 fg=blue bold
style 118-118 dim style 119-119 dim
5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ " 9| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
style 1-1 dim style 0-0 dim
style 10-39 fg=blue style 9-38 fg=blue
style 118-118 dim style 119-119 dim
6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │e" 10| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │"
style 0-0 fg=bright-magenta bold style 0-0 dim
style 1-1 dim style 4-43 fg=blue
style 5-44 fg=blue style 119-119 dim
style 118-119 dim 11| "│ ▄███████████████████▄ ████████████▀ │"
7| " │ ▄███████████████████▄ ████████████▀ │ " style 0-0 dim
style 1-1 dim style 4-43 fg=blue
style 5-44 fg=blue style 119-119 dim
style 118-118 dim 12| "│ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │"
8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " style 0-0 dim
style 1-1 dim style 4-42 fg=blue
style 5-43 fg=blue style 119-119 dim
style 118-118 dim 13| "│ ▄███▀█████████████████████▄ ████▀▀ │"
9| " │ ▄███▀█████████████████████▄ ████▀▀ │ " style 0-0 dim
style 1-1 dim style 6-41 fg=blue
style 7-42 fg=blue style 119-119 dim
style 118-118 dim 14| "│ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │"
10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ " style 0-0 dim
style 1-1 dim style 7-40 fg=blue
style 8-41 fg=blue style 49-70 bold
style 50-71 bold style 119-119 dim
style 118-118 dim 15| "│ ███ ▀███████▀█ ▀███████ │"
11| " │ ███ ▀███████▀█ ▀███████ │ " style 0-0 dim
style 1-1 dim style 7-39 fg=blue
style 8-40 fg=blue style 119-119 dim
style 118-118 dim 16| "│ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的 │"
12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露 │ " style 0-0 dim
style 1-1 dim style 7-39 fg=blue
style 8-40 fg=blue style 119-119 dim
style 118-118 dim 17| "│ ▀███ ▀██████████████ 问题,也可能促使我们重新审视,甚至推翻已有的设计。 │"
13| " │ ▀███ ▀██████████████ 的问题,也可能促使我们重新审视,甚至推翻已有的设计。 │ " style 0-0 dim
style 1-1 dim style 8-39 fg=blue
style 9-40 fg=blue style 119-119 dim
style 118-118 dim 18| "│ ▀███▄ ▀███████████▀ │"
14| " │ ▀███▄ ▀███████████▀ │ " style 0-0 dim
style 1-1 dim style 8-38 fg=blue
style 9-39 fg=blue style 119-119 dim
style 118-118 dim 19| "│ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至 │"
15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚 │ " style 0-0 dim
style 1-1 dim style 9-38 fg=blue
style 10-39 fg=blue style 119-119 dim
style 118-118 dim 20| "│ █████▄ ███▄▄ ▀█████▄▄ 反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。每一 │"
16| " │ █████▄ ███▄▄ ▀█████▄▄ 至反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。 │ " style 0-0 dim
style 1-1 dim style 9-38 fg=blue
style 10-39 fg=blue style 119-119 dim
style 118-118 dim 21| "│ ▀█████████████▄▄▄▄█▀█████▀ 条反馈,都会帮助我们把它打磨得更好。 │"
17| " │ ▀█████████████▄▄▄▄█▀█████▀ 每一条反馈,都会帮助我们把它打磨得更好。 │ " style 0-0 dim
style 1-1 dim style 8-39 fg=blue
style 9-40 fg=blue style 119-119 dim
style 118-118 dim 22| "│ ▀▀███████████▀▀ │"
18| " │ ▀▀███████████▀▀ │ " style 0-0 dim
style 1-1 dim style 12-34 fg=blue
style 13-35 fg=blue style 119-119 dim
style 118-118 dim 23| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
19| " │ │ " style 0-119 dim
style 1-1 dim 24| "│ Enter 继续 │"
style 118-118 dim style 0-0 dim
20| " │ │ "
style 1-1 dim
style 118-118 dim
21| " │ │ "
style 1-1 dim
style 118-118 dim
22| " │ │ "
style 1-1 dim
style 118-118 dim
23| " │ │ "
style 1-1 dim
style 118-118 dim
24| " │ │ "
style 1-1 dim
style 118-118 dim
25| " │ │ "
style 1-1 dim
style 118-118 dim
26| " ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ "
style 1-118 dim
27| " │ Enter 继续 │ "
style 1-1 dim
style 54-64 fg=bright-magenta bold style 54-64 fg=bright-magenta bold
style 118-118 dim style 119-119 dim
28| " │ │ " 25| "│ │"
style 1-1 dim style 0-0 dim
style 118-118 dim style 119-119 dim
29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " 26| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 1-118 dim style 0-119 dim
30-31| <blank> 27-31| <blank>

View File

@@ -6,101 +6,89 @@ viewport
2| " DEEPSEEK HARNESS" 2| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold style 1-8 fg=bright-magenta bold
style 10-16 bold style 10-16 bold
3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " 3| " scripted TUI ready."
style 1-158 dim style 1-19 dim
4| " │ ▄ DeepSeek Harness │ " 4| " main-session-{{uuid}}"
style 1-1 dim style 1-49 dim
style 11-38 fg=blue 5| <blank>
6| "/tmp/dsh-tui-welcome tui-scripted-model ↑0 ↓0 0% context"
style 0-82 fg=bright-magenta bold
style 85-102 dim
style 105-109 dim
style 112-121 dim
7| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-159 dim
8| "│ ▄ DeepSeek Harness │"
style 0-0 dim
style 10-37 fg=blue
style 95-110 fg=blue bold style 95-110 fg=blue bold
style 158-158 dim style 159-159 dim
5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ " 9| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
style 1-1 dim style 0-0 dim
style 10-39 fg=blue style 9-38 fg=blue
style 158-158 dim style 159-159 dim
6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " 10| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │"
style 0-0 fg=bright-magenta bold style 0-0 dim
style 1-1 dim style 4-43 fg=blue
style 5-44 fg=blue style 159-159 dim
style 158-158 dim 11| "│ ▄███████████████████▄ ████████████▀ │"
7| " │ ▄███████████████████▄ ████████████▀ │ " style 0-0 dim
style 1-1 dim style 4-43 fg=blue
style 5-44 fg=blue style 159-159 dim
style 158-158 dim 12| "│ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │"
8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " style 0-0 dim
style 1-1 dim style 4-42 fg=blue
style 5-43 fg=blue style 159-159 dim
style 158-158 dim 13| "│ ▄███▀█████████████████████▄ ████▀▀ │"
9| " │ ▄███▀█████████████████████▄ ████▀▀ │ " style 0-0 dim
style 1-1 dim style 6-41 fg=blue
style 7-42 fg=blue style 159-159 dim
style 158-158 dim 14| "│ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │"
10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ " style 0-0 dim
style 1-1 dim style 7-40 fg=blue
style 8-41 fg=blue style 49-70 bold
style 50-71 bold style 159-159 dim
style 158-158 dim 15| "│ ███ ▀███████▀█ ▀███████ │"
11| " │ ███ ▀███████▀█ ▀███████ │ " style 0-0 dim
style 1-1 dim style 7-39 fg=blue
style 8-40 fg=blue style 159-159 dim
style 158-158 dim 16| "│ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也可能促使我们重新审视,甚至推翻已 │"
12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也可能促使我们重新审视,甚至推翻 │ " style 0-0 dim
style 1-1 dim style 7-39 fg=blue
style 8-40 fg=blue style 159-159 dim
style 158-158 dim 17| "│ ▀███ ▀██████████████ 有的设计。 │"
13| " │ ▀███ ▀██████████████ 已有的设计。 │ " style 0-0 dim
style 1-1 dim style 8-39 fg=blue
style 9-40 fg=blue style 159-159 dim
style 158-158 dim 18| "│ ▀███▄ ▀███████████▀ │"
14| " │ ▀███▄ ▀███████████▀ │ " style 0-0 dim
style 1-1 dim style 8-38 fg=blue
style 9-39 fg=blue style 159-159 dim
style 158-158 dim 19| "│ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中留 │"
15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中 │ " style 0-0 dim
style 1-1 dim style 9-38 fg=blue
style 10-39 fg=blue style 159-159 dim
style 158-158 dim 20| "│ █████▄ ███▄▄ ▀█████▄▄ 言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。 │"
16| " │ █████▄ ███▄▄ ▀█████▄▄ 留言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。 │ " style 0-0 dim
style 1-1 dim style 9-38 fg=blue
style 10-39 fg=blue style 159-159 dim
style 158-158 dim 21| "│ ▀█████████████▄▄▄▄█▀█████▀ │"
17| " │ ▀█████████████▄▄▄▄█▀█████▀ │ " style 0-0 dim
style 1-1 dim style 8-39 fg=blue
style 9-40 fg=blue style 159-159 dim
style 158-158 dim 22| "│ ▀▀███████████▀▀ │"
18| " │ ▀▀███████████▀▀ │ " style 0-0 dim
style 1-1 dim style 12-34 fg=blue
style 13-35 fg=blue style 159-159 dim
style 158-158 dim 23| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
19| " │ │ " style 0-159 dim
style 1-1 dim 24| "│ Enter 继续 │"
style 158-158 dim style 0-0 dim
20| " │ │ "
style 1-1 dim
style 158-158 dim
21| " │ │ "
style 1-1 dim
style 158-158 dim
22| " │ │ "
style 1-1 dim
style 158-158 dim
23| " │ │ "
style 1-1 dim
style 158-158 dim
24| " │ │ "
style 1-1 dim
style 158-158 dim
25| " │ │ "
style 1-1 dim
style 158-158 dim
26| " ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ "
style 1-158 dim
27| " │ Enter 继续 │ "
style 1-1 dim
style 74-84 fg=bright-magenta bold style 74-84 fg=bright-magenta bold
style 158-158 dim style 159-159 dim
28| " │ │ " 25| "│ │"
style 1-1 dim style 0-0 dim
style 158-158 dim style 159-159 dim
29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " 26| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 1-158 dim style 0-159 dim
30-31| <blank> 27-31| <blank>

View File

@@ -0,0 +1,39 @@
terminal 60x12 buffer=normal length=16 base=4 viewport=4
lifecycle started=0 stopped=0 progress=inactive
title ""
cursor visible column=0 viewportRow=11 bufferRow=15
viewport
4| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
style 10-16 bold
5| "╭──────────────────────────────────────────────────────────╮"
style 0-59 dim
6| "│ DeepSeek Harness │"
style 0-0 dim
style 22-37 fg=blue bold
style 59-59 dim
7| "│ │"
style 0-0 dim
style 59-59 dim
8| "│ 感谢您愿意拨冗试用 DeepSeek Harness。 │"
style 0-0 dim
style 59-59 dim
9| "│ │"
style 0-0 dim
style 59-59 dim
10| "│ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验 │"
style 0-0 dim
style 59-59 dim
11| "├──────────────────────────────────────────────────────────┤"
style 0-59 dim
12| "│ Enter 继续 │"
style 0-0 dim
style 24-34 fg=bright-magenta bold
style 59-59 dim
13| "│ ↑/↓ 滚动 ↓ │"
style 0-0 dim
style 24-35 dim
style 59-59 dim
14| "╰──────────────────────────────────────────────────────────╯"
style 0-59 dim
15| <blank>

View File

@@ -6,95 +6,93 @@ viewport
4| " DEEPSEEK HARNESS" 4| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold style 1-8 fg=bright-magenta bold
style 10-16 bold style 10-16 bold
5| " ╭────────────────────────────────────────────────────────╮ " 5| "╭──────────────────────────────────────────────────────────╮"
style 1-58 dim style 0-59 dim
6| " │ ▄▄▄▄▄▄ ▄▄ │ " 6| "│ ▄▄▄▄▄▄ ▄▄ │"
style 1-1 dim style 0-0 dim
style 23-36 fg=blue style 23-36 fg=blue
style 58-58 dim style 59-59 dim
7| " │ ▄████████▄ ▀████▀ │ " 7| "│ ▄████████▄ ▀████▀ │"
style 1-1 dim style 0-0 dim
style 21-38 fg=blue style 21-38 fg=blue
style 58-58 dim style 59-59 dim
8| "/│ █▀▀▀▀███████▄██▀ │h" 8| "│ █▀▀▀▀███████▄██▀ │"
style 0-0 fg=bright-magenta bold style 0-0 dim
style 1-1 dim
style 22-37 fg=blue style 22-37 fg=blue
style 58-58 dim style 59-59 dim
style 59-59 fg=bright-magenta bold 9| "│ █▄ ▀███ ▀███ │"
9| " │ █▄ ▀███ ▀███ │ " style 0-0 dim
style 1-1 dim
style 22-36 fg=blue style 22-36 fg=blue
style 58-58 dim style 59-59 dim
10| " │ ▀█▄ ▀█████ │ " 10| "│ ▀█▄ ▀█████ │"
style 1-1 dim style 0-0 dim
style 23-36 fg=blue style 23-36 fg=blue
style 58-58 dim style 59-59 dim
11| " │ ▀█▄▄ █▄▄▀███▄ │ " 11| "│ ▀█▄▄ █▄▄▀███▄ │"
style 1-1 dim style 0-0 dim
style 23-36 fg=blue style 23-36 fg=blue
style 58-58 dim style 59-59 dim
12| " │ ▀▀▀▀▀▀ │ " 12| "│ ▀▀▀▀▀▀ │"
style 1-1 dim style 0-0 dim
style 25-34 fg=blue style 25-34 fg=blue
style 58-58 dim style 59-59 dim
13| " │ │ " 13| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
14| " │ DeepSeek Harness │ " 14| "│ DeepSeek Harness │"
style 1-1 dim style 0-0 dim
style 22-37 fg=blue bold style 22-37 fg=blue bold
style 58-58 dim style 59-59 dim
15| " │ │ " 15| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
16| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " 16| "│ 感谢您愿意拨冗试用 DeepSeek Harness。 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
17| " │ │ " 17| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
18| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体 │ " 18| "│ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
19| " │ 验难免粗粝。 │ " 19| "│ 难免粗粝。 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
20| " │ │ " 20| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
21| " │ “如切如磋,如琢如磨。” │ " 21| "│ “如切如磋,如琢如磨。” │"
style 1-1 dim style 0-0 dim
style 3-24 bold style 2-23 bold
style 58-58 dim style 59-59 dim
22| " │ │ " 22| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
23| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真 │ " 23| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
24| " │ 实使用中暴露的问题,也可能促使我们重新审视,甚至推翻已 │ " 24| "│ 使用中暴露的问题,也可能促使我们重新审视,甚至推翻已有的 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
25| " │ 有的设计。 │ " 25| "│ 设计。 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
26| " │ │ " 26| "│ │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
27| " │ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未 │ " 27| "│ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能 │"
style 1-1 dim style 0-0 dim
style 58-58 dim style 59-59 dim
28| " ├────────────────────────────────────────────────────────┤ " 28| "├──────────────────────────────────────────────────────────┤"
style 1-58 dim style 0-59 dim
29| " │ Enter 继续 │ " 29| "│ Enter 继续 │"
style 1-1 dim style 0-0 dim
style 24-34 fg=bright-magenta bold style 24-34 fg=bright-magenta bold
style 58-58 dim style 59-59 dim
30| " │ ↑/↓ 滚动 ↓ │ " 30| "│ ↑/↓ 滚动 ↓ │"
style 1-1 dim style 0-0 dim
style 24-35 dim style 24-35 dim
style 58-58 dim style 59-59 dim
31| " ╰────────────────────────────────────────────────────────╯ " 31| "╰──────────────────────────────────────────────────────────╯"
style 1-58 dim style 0-59 dim
32-33| <blank> 32-33| <blank>

View File

@@ -6,98 +6,96 @@ viewport
3| " DEEPSEEK HARNESS" 3| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold style 1-8 fg=bright-magenta bold
style 10-16 bold style 10-16 bold
4| " ╭────────────────────────────────────────────────────────────────────────────╮ " 4| "╭──────────────────────────────────────────────────────────────────────────────╮"
style 1-78 dim style 0-79 dim
5| " │ ▄▄▄▄▄▄▄██▀ █▄ ▄ │ " 5| "│ ▄▄▄▄▄▄▄██▀ █▄ ▄ │"
style 1-1 dim style 0-0 dim
style 26-53 fg=blue style 26-53 fg=blue
style 78-78 dim style 79-79 dim
6| " │ ▄███████████▄▄ ███▄▄████ │ " 6| "│ ▄███████████▄▄ ███▄▄████ │"
style 1-1 dim style 0-0 dim
style 26-53 fg=blue style 26-53 fg=blue
style 78-78 dim style 79-79 dim
7| "/│ ████████████████▄ ▀██████▀ │F" 7| "│ ████████████████▄ ▀██████▀ │"
style 0-0 fg=bright-magenta bold style 0-0 dim
style 1-1 dim
style 26-52 fg=blue style 26-52 fg=blue
style 78-78 dim style 79-79 dim
style 79-79 fg=bright-magenta bold 8| "│ ██▀▀▀▀▀████████████▄▄██▀ │"
8| " │ ██▀▀▀▀▀████████████▄▄██▀ │ " style 0-0 dim
style 1-1 dim
style 28-51 fg=blue style 28-51 fg=blue
style 78-78 dim style 79-79 dim
9| " │ ██ ▀█████▄ ▀█████ │ " 9| "│ ██ ▀█████▄ ▀█████ │"
style 1-1 dim style 0-0 dim
style 28-50 fg=blue style 28-50 fg=blue
style 78-78 dim style 79-79 dim
10| " │ ██▄ ▀████▄ ▄████ │ " 10| "│ ██▄ ▀████▄ ▄████ │"
style 1-1 dim style 0-0 dim
style 28-50 fg=blue style 28-50 fg=blue
style 78-78 dim style 79-79 dim
11| " │ ██▄ ████████▀ │ " 11| "│ ██▄ ████████▀ │"
style 1-1 dim style 0-0 dim
style 29-50 fg=blue style 29-50 fg=blue
style 78-78 dim style 79-79 dim
12| " │ ██▄ ▄▄ ▀█████▀ │ " 12| "│ ██▄ ▄▄ ▀█████▀ │"
style 1-1 dim style 0-0 dim
style 29-49 fg=blue style 29-49 fg=blue
style 78-78 dim style 79-79 dim
13| " │ ▀███▄▄▄███▄ ████▄▄ │ " 13| "│ ▀███▄▄▄███▄ ████▄▄ │"
style 1-1 dim style 0-0 dim
style 29-50 fg=blue style 29-50 fg=blue
style 78-78 dim style 79-79 dim
14| " │ ▀▀▀███████▀▀ │ " 14| "│ ▀▀▀███████▀▀ │"
style 1-1 dim style 0-0 dim
style 31-47 fg=blue style 31-47 fg=blue
style 78-78 dim style 79-79 dim
15| " │ │ " 15| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
16| " │ DeepSeek Harness │ " 16| "│ DeepSeek Harness │"
style 1-1 dim style 0-0 dim
style 32-47 fg=blue bold style 32-47 fg=blue bold
style 78-78 dim style 79-79 dim
17| " │ │ " 17| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
18| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " 18| "│ 感谢您愿意拨冗试用 DeepSeek Harness。 │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
19| " │ │ " 19| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
20| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " 20| "│ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
21| " │ │ " 21| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
22| " │ “如切如磋,如琢如磨。” │ " 22| "│ “如切如磋,如琢如磨。” │"
style 1-1 dim style 0-0 dim
style 3-24 bold style 2-23 bold
style 78-78 dim style 79-79 dim
23| " │ │ " 23| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
24| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题, │ " 24| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也 │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
25| " │ 也可能促使我们重新审视,甚至推翻已有的设计。 │ " 25| "│ 可能促使我们重新审视,甚至推翻已有的设计。 │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
26| " │ │ " 26| "│ │"
style 1-1 dim style 0-0 dim
style 78-78 dim style 79-79 dim
27| " ├────────────────────────────────────────────────────────────────────────────┤ " 27| "├──────────────────────────────────────────────────────────────────────────────┤"
style 1-78 dim style 0-79 dim
28| " │ Enter 继续 │ " 28| "│ Enter 继续 │"
style 1-1 dim style 0-0 dim
style 34-44 fg=bright-magenta bold style 34-44 fg=bright-magenta bold
style 78-78 dim style 79-79 dim
29| " │ ↑/↓ 滚动 ↓ │ " 29| "│ ↑/↓ 滚动 ↓ │"
style 1-1 dim style 0-0 dim
style 34-45 dim style 34-45 dim
style 78-78 dim style 79-79 dim
30| " ╰────────────────────────────────────────────────────────────────────────────╯ " 30| "╰──────────────────────────────────────────────────────────────────────────────╯"
style 1-78 dim style 0-79 dim
31-32| <blank> 31-32| <blank>

View File

@@ -3,11 +3,14 @@ import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Context } from 'cordis'
import { visibleWidth } from '@earendil-works/pi-tui' import { visibleWidth } from '@earendil-works/pi-tui'
import type { TuiOverlayHost, TuiTheme } from '@deepseek-ai/dsh-tui' import type { TuiOverlayHost, TuiOverlayRequest, TuiTheme } from '@deepseek-ai/dsh-tui'
import { import {
acknowledgeTuiFirstRunWelcome, acknowledgeTuiFirstRunWelcome,
apply,
hasTuiFirstRunWelcomeAcknowledgement, hasTuiFirstRunWelcomeAcknowledgement,
needsTuiFirstRunWelcomeAsciiArt,
TuiFirstRunWelcomeComponent, TuiFirstRunWelcomeComponent,
tuiFirstRunWelcomeAcknowledgementPath, tuiFirstRunWelcomeAcknowledgementPath,
tuiFirstRunWelcomeArtTier, tuiFirstRunWelcomeArtTier,
@@ -98,6 +101,16 @@ describe('TUI first-run welcome acknowledgement', () => {
recursive: true, recursive: true,
}) })
await expect(hasTuiFirstRunWelcomeAcknowledgement(home)).rejects.toThrow('is not a file') await expect(hasTuiFirstRunWelcomeAcknowledgement(home)).rejects.toThrow('is not a file')
await expect(acknowledgeTuiFirstRunWelcome(home)).rejects.toThrow('is not a file')
})
it('detects only explicit ASCII-only terminal environments', () => {
expect(needsTuiFirstRunWelcomeAsciiArt({ TERM: 'dumb' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_ALL: 'C' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_CTYPE: 'POSIX' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'C' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'en_US.UTF-8' })).toBe(false)
expect(typeof needsTuiFirstRunWelcomeAsciiArt()).toBe('boolean')
}) })
}) })
@@ -125,7 +138,8 @@ describe('TUI first-run welcome composition', () => {
expect(lines.every(line => visibleWidth(line) <= renderWidth)).toBe(true) expect(lines.every(line => visibleWidth(line) <= renderWidth)).toBe(true)
expect(lines.join('\n')).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim()) expect(lines.join('\n')).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim())
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`) expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
expect(lines).toHaveLength(Math.floor(rows * 0.9)) expect(lines.length).toBeLessThanOrEqual(Math.floor(rows * 0.9))
expect(lines.length).toBeGreaterThan(5)
}) })
it('drops the whale at low height while keeping prose, scrolling, and Enter reachable', () => { it('drops the whale at low height while keeping prose, scrolling, and Enter reachable', () => {
@@ -140,6 +154,19 @@ describe('TUI first-run welcome composition', () => {
const end = component.render(54).join('\n') const end = component.render(54).join('\n')
expect(withoutWhitespace(end)).toContain(withoutWhitespace(copy.paragraphs.at(-1)!.slice(-10))) expect(withoutWhitespace(end)).toContain(withoutWhitespace(copy.paragraphs.at(-1)!.slice(-10)))
expect(end).toContain(`Enter ${copy.continueLabel}`) expect(end).toContain(`Enter ${copy.continueLabel}`)
for (const key of ['\x1b[A', '\x1b[B', '\x1b[5~', '\x1b[6~', '\x1b[H', 'x']) {
component.handleInput(key)
}
component.invalidate()
})
it('renders a tiny viewport and a quotation-only paragraph without overdraw', () => {
const fixture = hostFixture(5)
const quoteOnly = { ...copy, paragraphs: ['“如切如磋,如琢如磨。”'] }
const component = new TuiFirstRunWelcomeComponent(fixture.host, quoteOnly, async () => {})
const lines = component.render(2)
expect(lines.every(line => visibleWidth(line) <= 6)).toBe(true)
}) })
it('renders the bit-equivalent ASCII icon fallback for an explicitly non-Unicode terminal', () => { it('renders the bit-equivalent ASCII icon fallback for an explicitly non-Unicode terminal', () => {
@@ -166,6 +193,23 @@ describe('TUI first-run welcome composition', () => {
expect(acknowledge).toHaveBeenCalledOnce() expect(acknowledge).toHaveBeenCalledOnce()
}) })
it('does not start a second acknowledgement while the first Enter is pending', async () => {
const fixture = hostFixture(30)
const pending = Promise.withResolvers<undefined>()
const acknowledge = vi.fn(async () => pending.promise)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge)
component.render(72)
component.handleInput('\r')
component.handleInput('\r')
component.handleInput('\x1b[B')
expect(component.render(72).join('\n')).toContain(copy.saving)
expect(acknowledge).toHaveBeenCalledOnce()
pending.resolve(undefined)
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
})
it('keeps the overlay open after a persistence failure and lets Enter retry', async () => { it('keeps the overlay open after a persistence failure and lets Enter retry', async () => {
const fixture = hostFixture(30) const fixture = hostFixture(30)
let attempts = 0 let attempts = 0
@@ -186,4 +230,36 @@ describe('TUI first-run welcome composition', () => {
expect(attempts).toBe(2) expect(attempts).toBe(2)
expect(fixture.invalidations()).toBeGreaterThanOrEqual(3) expect(fixture.invalidations()).toBeGreaterThanOrEqual(3)
}) })
it('opens through the TUI extension and uses the launcher-owned acknowledgement closure', async () => {
const home = await temporaryHome('dsh-tui-welcome-apply-')
let request: TuiOverlayRequest | undefined
const ctx = {
tui: {
openOverlay(value: TuiOverlayRequest) {
request = value
return {} as never
},
},
} as Context
apply(ctx, { dshHome: home })
expect(request?.options).toEqual({
width: '100%',
maxHeight: '90%',
anchor: 'center',
margin: 0,
})
const fixture = hostFixture(30)
const component = request?.create(fixture.host)
expect(component).toBeInstanceOf(TuiFirstRunWelcomeComponent)
component?.handleInput?.('\r')
await vi.waitFor(async () => {
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(true)
})
apply(ctx, { dshHome: home, asciiArt: true })
expect(request?.create(fixture.host).render(72).join('\n'))
.toContain(TUI_FIRST_RUN_WELCOME_WHALE.compact.ascii[0]!.trim())
})
}) })

View File

@@ -180,7 +180,9 @@ async function firstRunFrameSnapshot(
const terminal = new HeadlessTerminal(columns, rows) const terminal = new HeadlessTerminal(columns, rows)
try { try {
terminal.write(output.slice(0, frameEnd + synchronizedFrameEnd.length)) terminal.write(output.slice(0, frameEnd + synchronizedFrameEnd.length))
return await terminal.snapshot() return (await terminal.snapshot())
.replace(/main-session-[0-9a-f-]{36}/gu, 'main-session-{{uuid}}')
.replace(/\/[^"\s]*dsh-tui-welcome-\d+-[A-Za-z0-9]+/gu, '/tmp/dsh-tui-welcome')
} finally { } finally {
await terminal.dispose() await terminal.dispose()
} }
@@ -206,11 +208,14 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
tempDirPrefix: `dsh-tui-welcome-${String(columns)}-`, tempDirPrefix: `dsh-tui-welcome-${String(columns)}-`,
configPath: scriptedConfigPath, configPath: scriptedConfigPath,
showFirstRunWelcome: true, showFirstRunWelcome: true,
expectedExitCode: process.platform === 'win32' ? 0 : -15,
columns, columns,
rows: 30, rows: 30,
actions: [ actions: [
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' }, { waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' }, columns >= 120
? { waitFor: firstRunCopy.saving, signal: 'SIGTERM', delayMs: 500 }
: { waitFor: 'main-session-', occurrence: 2, signal: 'SIGTERM' },
], ],
inspect: async (cwd) => { inspect: async (cwd) => {
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(cwd, '.dsh'))).toBe(true) expect(await hasTuiFirstRunWelcomeAcknowledgement(join(cwd, '.dsh'))).toBe(true)
@@ -226,7 +231,6 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
.toMatchFileSnapshot(join(firstRunSnapshots, `${String(columns)}-columns.expected.txt`)) .toMatchFileSnapshot(join(firstRunSnapshots, `${String(columns)}-columns.expected.txt`))
expect(output).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim()) expect(output).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim())
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`) expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS) }, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('keeps prose and Enter reachable in a low-height real PTY after dropping the whale', async () => { it('keeps prose and Enter reachable in a low-height real PTY after dropping the whale', async () => {
@@ -235,12 +239,13 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
tempDirPrefix: 'dsh-tui-welcome-low-', tempDirPrefix: 'dsh-tui-welcome-low-',
configPath: scriptedConfigPath, configPath: scriptedConfigPath,
showFirstRunWelcome: true, showFirstRunWelcome: true,
expectedExitCode: process.platform === 'win32' ? 0 : -15,
columns: 60, columns: 60,
rows: 12, rows: 12,
actions: [ actions: [
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\x1b[F' }, { waitFor: firstRunCopy.paragraphs[0]!, send: '\x1b[F' },
{ waitFor: '企业微信群', send: '\r' }, { waitFor: '企业微信群', send: '\r' },
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' }, { waitFor: 'main-session-', occurrence: 2, signal: 'SIGTERM' },
], ],
}) })
await expect(await firstRunFrameSnapshot(output, firstRunCopy.paragraphs[0]!, 60, 12)) await expect(await firstRunFrameSnapshot(output, firstRunCopy.paragraphs[0]!, 60, 12))
@@ -261,9 +266,10 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
cwd, cwd,
configPath: scriptedConfigPath, configPath: scriptedConfigPath,
showFirstRunWelcome: true, showFirstRunWelcome: true,
expectedExitCode: process.platform === 'win32' ? 0 : -15,
actions: [ actions: [
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' }, { waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' }, { waitFor: firstRunCopy.saving, signal: 'SIGTERM', delayMs: 500 },
], ],
}) })
expect(first).toContain(firstRunCopy.title) expect(first).toContain(firstRunCopy.title)
@@ -274,7 +280,8 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
cwd, cwd,
configPath: scriptedConfigPath, configPath: scriptedConfigPath,
showFirstRunWelcome: true, showFirstRunWelcome: true,
actions: [{ waitFor: 'main-session-', send: '/exit\r', delayMs: 1_500 }], expectedExitCode: process.platform === 'win32' ? 0 : -15,
actions: [{ waitFor: 'main-session-', signal: 'SIGTERM' }],
}) })
expect(second).not.toContain(firstRunCopy.paragraphs[0]) expect(second).not.toContain(firstRunCopy.paragraphs[0])
expect(second).not.toContain(`Enter ${firstRunCopy.continueLabel}`) expect(second).not.toContain(`Enter ${firstRunCopy.continueLabel}`)
@@ -283,6 +290,40 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
} }
}, LOADER_SMOKE_TEST_TIMEOUT_MS) }, LOADER_SMOKE_TEST_TIMEOUT_MS)
it.skipIf(process.platform === 'win32')('keeps the notice eligible when the process exits before Enter', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-welcome-abort-'))
try {
await smoke({
label: 'dsh aborted welcome launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: -15,
actions: [{ waitFor: firstRunCopy.paragraphs[0]!, signal: 'SIGTERM' }],
inspect: async (workspace) => {
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(workspace, '.dsh'))).toBe(false)
},
})
const next = await smoke({
label: 'dsh welcome after aborted launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: -15,
actions: [
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
{ waitFor: firstRunCopy.saving, signal: 'SIGTERM', delayMs: 500 },
],
})
expect(next).toContain(firstRunCopy.paragraphs[0])
} finally {
await rm(cwd, { recursive: true, force: true })
}
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal', async () => { it('boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal', async () => {
// With no configured welcome the borderless banner sweeps in left-to-right; // With no configured welcome the borderless banner sweeps in left-to-right;
// the detail line's session id (`main-session-<uuid>`) renders only once // the detail line's session id (`main-session-<uuid>`) renders only once
@@ -462,6 +503,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
binScript: dshBinScript, binScript: dshBinScript,
configArgs: ['--resume', 'resume-target', '--config', scriptedConfigPath], configArgs: ['--resume', 'resume-target', '--config', scriptedConfigPath],
showFirstRunWelcome: true, showFirstRunWelcome: true,
expectedExitCode: process.platform === 'win32' ? 0 : -15,
prepare: async (cwd) => { prepare: async (cwd) => {
await seedResumeSession(cwd) await seedResumeSession(cwd)
const before = await readFile(logPath( const before = await readFile(logPath(
@@ -474,7 +516,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
}, },
actions: [ actions: [
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' }, { waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
{ waitFor: 'resume-target', occurrence: 2, send: '/exit\r' }, { waitFor: firstRunCopy.saving, signal: 'SIGTERM', delayMs: 500 },
], ],
inspect: async (cwd) => { inspect: async (cwd) => {
const after = await readFile(logPath( const after = await readFile(logPath(
@@ -486,11 +528,6 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expect(after).not.toContain(firstRunCopy.paragraphs[0]) expect(after).not.toContain(firstRunCopy.paragraphs[0])
const appended = after.split('\n').filter(Boolean).slice(originalLineCount) const appended = after.split('\n').filter(Boolean).slice(originalLineCount)
.map(line => JSON.parse(line) as SessionEvent) .map(line => JSON.parse(line) as SessionEvent)
expect(appended.map(event => event.type)).toEqual([
'session/end-seed',
'command/run',
'command/done',
])
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'user/message' })) expect(appended).not.toContainEqual(expect.objectContaining({ type: 'user/message' }))
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'turn/start' })) expect(appended).not.toContainEqual(expect.objectContaining({ type: 'turn/start' }))
}, },