diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.i18n.yaml index ff8ce65c14..278a28bba3 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.i18n.yaml @@ -2,5 +2,5 @@ # 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: # 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.zh.md: 7760ebde0e117e0f03d572b8263330b24c5a81ab +2026-07-30-versioned-tui-first-run-welcome.md: 35ac5718a7b7665e903a3975acffbc2711000aa1 +2026-07-30-versioned-tui-first-run-welcome.zh.md: f6edf354e42f2797e1227ed04a3fbc9701a04b8b diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md index d5635aebd6..35ac5718a7 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md @@ -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 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 diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.zh.md index 7760ebde0e..f6edf354e4 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.zh.md @@ -18,7 +18,7 @@ Status: implemented 指定的官方 `24x24` DeepSeek SVG 作为视觉真源提交。静态的完整、紧凑和最小终端栅格图以逐级降低的方形分辨率对该精确路径取样,不会重新绘制轮廓。Unicode `▀`/`▄`/`█` 单元格让每个终端单元格保留两个垂直方向的源像素;明确仅支持 ASCII 的 locale 则使用位级等价的 `'`/`_`/`#` 回退。ANSI 样式与 SVG 和可编辑文案完全分离:`ctx.tui` 提供语义化 `brand` 角色,在真彩色可用时使用官方 `#4D6BFE` 色值,否则使用标准 ANSI 蓝色;禁用颜色时则使用纯文本。普通启动 banner 保留现有渐变。 -浮层居中显示,并使用可用的终端宽度。宽终端将完整图标置于标题与正文旁边;中等和窄终端把紧凑或最小图标堆叠在正文上方;高度不足时,先移除图标,再减少正文空间。正文可以滚动,而标题和唯一操作保持固定。引用内容会提升为独立的视觉段落,但不会改变集中持有的中文字符串。通过 Enter 关闭浮层后,模态所有权会交还给现有 FIFO 管理器;该管理器恢复编辑器,同时保持正常的启动 banner、transcript(文本记录)和焦点行为不变。 +浮层居中显示,并使用可用的终端宽度;高度则跟随实际内容,仅将 viewport 的 90% 作为上限。宽终端将完整图标置于标题与正文旁边;中等和窄终端把紧凑或最小图标堆叠在正文上方;高度不足时,先移除图标,再减少正文空间。正文可以滚动,而标题和唯一操作保持固定。引用内容会提升为独立的视觉段落,但不会改变集中持有的中文字符串。通过 Enter 关闭浮层后,模态所有权会交还给现有 FIFO 管理器;该管理器恢复编辑器,同时保持正常的启动 banner、transcript(文本记录)和焦点行为不变。 ## 验证 diff --git a/apps/cli/src/tui-first-run-welcome.ts b/apps/cli/src/tui-first-run-welcome.ts index 5d7cd63198..d69d35a30a 100644 --- a/apps/cli/src/tui-first-run-welcome.ts +++ b/apps/cli/src/tui-first-run-welcome.ts @@ -115,9 +115,12 @@ export async function acknowledgeTuiFirstRunWelcome( try { handle = await open(path, 'wx', 0o600) } catch (error) { + /* v8 ignore else -- the only expected race is another creator publishing this exact marker */ 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 } + /* v8 ignore next -- unexpected filesystem failures pass through unchanged */ throw error } try { @@ -127,10 +130,14 @@ export async function acknowledgeTuiFirstRunWelcome( await created.close() await syncDirectory(directory) } 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 - } finally { - await handle?.close() + /* v8 ignore stop */ } } @@ -170,7 +177,7 @@ export function tuiFirstRunWelcomeArtTier( innerWidth: number, viewportRows: number, ): 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 >= 48 && viewportRows >= 14) return 'minimal' return undefined @@ -250,12 +257,13 @@ export class TuiFirstRunWelcomeComponent implements TuiComponent, TuiFocusable { 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) this.maxScrollOffset = maxOffset this.scrollOffset = Math.min(this.scrollOffset, maxOffset) 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 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 ? [...fixedHeader, ...visibleBody] - : visibleBody.map((line, index) => { + : Array.from({ length: Math.max(fullArt.length, visibleBody.length) }, (_, index) => { const art = fullArt[index] ?? '' + const line = visibleBody[index] ?? '' const left = `${art}${' '.repeat(Math.max(0, fullArtWidth - visibleWidth(art)))}` return `${left} ${line}` }) @@ -346,7 +355,7 @@ export function apply(ctx: Context, config: Config): void { width: '100%', maxHeight: '90%', anchor: 'center', - margin: 1, + margin: 0, }, }) } diff --git a/apps/cli/tests/pty-harness.ts b/apps/cli/tests/pty-harness.ts index 202fb20470..fb926bd7c1 100644 --- a/apps/cli/tests/pty-harness.ts +++ b/apps/cli/tests/pty-harness.ts @@ -44,7 +44,9 @@ while time.monotonic() < deadline: action = actions[action_index] if action.get("delayMs", 0) > 0: 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"]) os.makedirs(os.path.dirname(target), exist_ok=True) 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. */ type TuiPtyAction = | { 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 occurrence?: number @@ -190,7 +193,9 @@ async function runWindowsPtySmoke( && output.split(actions[actionIndex]!.waitFor).length - 1 >= (actions[actionIndex]!.occurrence ?? 1) ) { 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) mkdirSync(dirname(target), { recursive: true }) writeFileSync(target, action.writeFile.content) diff --git a/apps/cli/tests/snapshots/tui-first-run-welcome/120-columns.expected.txt b/apps/cli/tests/snapshots/tui-first-run-welcome/120-columns.expected.txt index 7790624f12..a22b548e5a 100644 --- a/apps/cli/tests/snapshots/tui-first-run-welcome/120-columns.expected.txt +++ b/apps/cli/tests/snapshots/tui-first-run-welcome/120-columns.expected.txt @@ -6,101 +6,89 @@ viewport 2| " DEEPSEEK HARNESS" style 1-8 fg=bright-magenta bold style 10-16 bold -3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " - style 1-118 dim -4| " │ ▄ DeepSeek Harness │ " - style 1-1 dim - style 11-38 fg=blue +3| " scripted TUI ready." + style 1-19 dim +4| " main-session-{{uuid}}" + style 1-49 dim +5| +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 118-118 dim -5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ " - style 1-1 dim - style 10-39 fg=blue - style 118-118 dim -6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │e" - style 0-0 fg=bright-magenta bold - style 1-1 dim - style 5-44 fg=blue - style 118-119 dim -7| " │ ▄███████████████████▄ ████████████▀ │ " - style 1-1 dim - style 5-44 fg=blue - style 118-118 dim -8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " - style 1-1 dim - style 5-43 fg=blue - style 118-118 dim -9| " │ ▄███▀█████████████████████▄ ████▀▀ │ " - style 1-1 dim - style 7-42 fg=blue - style 118-118 dim -10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ " - style 1-1 dim - style 8-41 fg=blue - style 50-71 bold - style 118-118 dim -11| " │ ███ ▀███████▀█ ▀███████ │ " - style 1-1 dim - style 8-40 fg=blue - style 118-118 dim -12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露 │ " - style 1-1 dim - style 8-40 fg=blue - style 118-118 dim -13| " │ ▀███ ▀██████████████ 的问题,也可能促使我们重新审视,甚至推翻已有的设计。 │ " - style 1-1 dim - style 9-40 fg=blue - style 118-118 dim -14| " │ ▀███▄ ▀███████████▀ │ " - style 1-1 dim - style 9-39 fg=blue - style 118-118 dim -15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚 │ " - style 1-1 dim - style 10-39 fg=blue - style 118-118 dim -16| " │ █████▄ ███▄▄ ▀█████▄▄ 至反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。 │ " - style 1-1 dim - style 10-39 fg=blue - style 118-118 dim -17| " │ ▀█████████████▄▄▄▄█▀█████▀ 每一条反馈,都会帮助我们把它打磨得更好。 │ " - style 1-1 dim - style 9-40 fg=blue - style 118-118 dim -18| " │ ▀▀███████████▀▀ │ " - style 1-1 dim - style 13-35 fg=blue - style 118-118 dim -19| " │ │ " - style 1-1 dim - style 118-118 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 119-119 dim +9| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │" + style 0-0 dim + style 9-38 fg=blue + style 119-119 dim +10| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │" + style 0-0 dim + style 4-43 fg=blue + style 119-119 dim +11| "│ ▄███████████████████▄ ████████████▀ │" + style 0-0 dim + style 4-43 fg=blue + style 119-119 dim +12| "│ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │" + style 0-0 dim + style 4-42 fg=blue + style 119-119 dim +13| "│ ▄███▀█████████████████████▄ ████▀▀ │" + style 0-0 dim + style 6-41 fg=blue + style 119-119 dim +14| "│ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │" + style 0-0 dim + style 7-40 fg=blue + style 49-70 bold + style 119-119 dim +15| "│ ███ ▀███████▀█ ▀███████ │" + style 0-0 dim + style 7-39 fg=blue + style 119-119 dim +16| "│ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的 │" + style 0-0 dim + style 7-39 fg=blue + style 119-119 dim +17| "│ ▀███ ▀██████████████ 问题,也可能促使我们重新审视,甚至推翻已有的设计。 │" + style 0-0 dim + style 8-39 fg=blue + style 119-119 dim +18| "│ ▀███▄ ▀███████████▀ │" + style 0-0 dim + style 8-38 fg=blue + style 119-119 dim +19| "│ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至 │" + style 0-0 dim + style 9-38 fg=blue + style 119-119 dim +20| "│ █████▄ ███▄▄ ▀█████▄▄ 反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。每一 │" + style 0-0 dim + style 9-38 fg=blue + style 119-119 dim +21| "│ ▀█████████████▄▄▄▄█▀█████▀ 条反馈,都会帮助我们把它打磨得更好。 │" + style 0-0 dim + style 8-39 fg=blue + style 119-119 dim +22| "│ ▀▀███████████▀▀ │" + style 0-0 dim + style 12-34 fg=blue + style 119-119 dim +23| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤" + style 0-119 dim +24| "│ Enter 继续 │" + style 0-0 dim style 54-64 fg=bright-magenta bold - style 118-118 dim -28| " │ │ " - style 1-1 dim - style 118-118 dim -29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " - style 1-118 dim -30-31| + style 119-119 dim +25| "│ │" + style 0-0 dim + style 119-119 dim +26| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" + style 0-119 dim +27-31| diff --git a/apps/cli/tests/snapshots/tui-first-run-welcome/160-columns.expected.txt b/apps/cli/tests/snapshots/tui-first-run-welcome/160-columns.expected.txt index 14689f9b69..4c22bfd6cb 100644 --- a/apps/cli/tests/snapshots/tui-first-run-welcome/160-columns.expected.txt +++ b/apps/cli/tests/snapshots/tui-first-run-welcome/160-columns.expected.txt @@ -6,101 +6,89 @@ viewport 2| " DEEPSEEK HARNESS" style 1-8 fg=bright-magenta bold style 10-16 bold -3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " - style 1-158 dim -4| " │ ▄ DeepSeek Harness │ " - style 1-1 dim - style 11-38 fg=blue +3| " scripted TUI ready." + style 1-19 dim +4| " main-session-{{uuid}}" + style 1-49 dim +5| +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 158-158 dim -5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ " - style 1-1 dim - style 10-39 fg=blue - style 158-158 dim -6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " - style 0-0 fg=bright-magenta bold - style 1-1 dim - style 5-44 fg=blue - style 158-158 dim -7| " │ ▄███████████████████▄ ████████████▀ │ " - style 1-1 dim - style 5-44 fg=blue - style 158-158 dim -8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " - style 1-1 dim - style 5-43 fg=blue - style 158-158 dim -9| " │ ▄███▀█████████████████████▄ ████▀▀ │ " - style 1-1 dim - style 7-42 fg=blue - style 158-158 dim -10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ " - style 1-1 dim - style 8-41 fg=blue - style 50-71 bold - style 158-158 dim -11| " │ ███ ▀███████▀█ ▀███████ │ " - style 1-1 dim - style 8-40 fg=blue - style 158-158 dim -12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也可能促使我们重新审视,甚至推翻 │ " - style 1-1 dim - style 8-40 fg=blue - style 158-158 dim -13| " │ ▀███ ▀██████████████ 已有的设计。 │ " - style 1-1 dim - style 9-40 fg=blue - style 158-158 dim -14| " │ ▀███▄ ▀███████████▀ │ " - style 1-1 dim - style 9-39 fg=blue - style 158-158 dim -15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中 │ " - style 1-1 dim - style 10-39 fg=blue - style 158-158 dim -16| " │ █████▄ ███▄▄ ▀█████▄▄ 留言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。 │ " - style 1-1 dim - style 10-39 fg=blue - style 158-158 dim -17| " │ ▀█████████████▄▄▄▄█▀█████▀ │ " - style 1-1 dim - style 9-40 fg=blue - style 158-158 dim -18| " │ ▀▀███████████▀▀ │ " - style 1-1 dim - style 13-35 fg=blue - style 158-158 dim -19| " │ │ " - style 1-1 dim - style 158-158 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 159-159 dim +9| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │" + style 0-0 dim + style 9-38 fg=blue + style 159-159 dim +10| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │" + style 0-0 dim + style 4-43 fg=blue + style 159-159 dim +11| "│ ▄███████████████████▄ ████████████▀ │" + style 0-0 dim + style 4-43 fg=blue + style 159-159 dim +12| "│ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │" + style 0-0 dim + style 4-42 fg=blue + style 159-159 dim +13| "│ ▄███▀█████████████████████▄ ████▀▀ │" + style 0-0 dim + style 6-41 fg=blue + style 159-159 dim +14| "│ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │" + style 0-0 dim + style 7-40 fg=blue + style 49-70 bold + style 159-159 dim +15| "│ ███ ▀███████▀█ ▀███████ │" + style 0-0 dim + style 7-39 fg=blue + style 159-159 dim +16| "│ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也可能促使我们重新审视,甚至推翻已 │" + style 0-0 dim + style 7-39 fg=blue + style 159-159 dim +17| "│ ▀███ ▀██████████████ 有的设计。 │" + style 0-0 dim + style 8-39 fg=blue + style 159-159 dim +18| "│ ▀███▄ ▀███████████▀ │" + style 0-0 dim + style 8-38 fg=blue + style 159-159 dim +19| "│ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中留 │" + style 0-0 dim + style 9-38 fg=blue + style 159-159 dim +20| "│ █████▄ ███▄▄ ▀█████▄▄ 言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。 │" + style 0-0 dim + style 9-38 fg=blue + style 159-159 dim +21| "│ ▀█████████████▄▄▄▄█▀█████▀ │" + style 0-0 dim + style 8-39 fg=blue + style 159-159 dim +22| "│ ▀▀███████████▀▀ │" + style 0-0 dim + style 12-34 fg=blue + style 159-159 dim +23| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤" + style 0-159 dim +24| "│ Enter 继续 │" + style 0-0 dim style 74-84 fg=bright-magenta bold - style 158-158 dim -28| " │ │ " - style 1-1 dim - style 158-158 dim -29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " - style 1-158 dim -30-31| + style 159-159 dim +25| "│ │" + style 0-0 dim + style 159-159 dim +26| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" + style 0-159 dim +27-31| diff --git a/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns-low-height.expected.txt b/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns-low-height.expected.txt new file mode 100644 index 0000000000..dc0459ab43 --- /dev/null +++ b/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns-low-height.expected.txt @@ -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| diff --git a/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns.expected.txt b/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns.expected.txt index 47a3b3c8a0..8cbafd5d27 100644 --- a/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns.expected.txt +++ b/apps/cli/tests/snapshots/tui-first-run-welcome/60-columns.expected.txt @@ -6,95 +6,93 @@ viewport 4| " DEEPSEEK HARNESS" style 1-8 fg=bright-magenta bold style 10-16 bold -5| " ╭────────────────────────────────────────────────────────╮ " - style 1-58 dim -6| " │ ▄▄▄▄▄▄ ▄▄ │ " - style 1-1 dim +5| "╭──────────────────────────────────────────────────────────╮" + style 0-59 dim +6| "│ ▄▄▄▄▄▄ ▄▄ │" + style 0-0 dim style 23-36 fg=blue - style 58-58 dim -7| " │ ▄████████▄ ▀████▀ │ " - style 1-1 dim + style 59-59 dim +7| "│ ▄████████▄ ▀████▀ │" + style 0-0 dim style 21-38 fg=blue - style 58-58 dim -8| "/│ █▀▀▀▀███████▄██▀ │h" - style 0-0 fg=bright-magenta bold - style 1-1 dim + style 59-59 dim +8| "│ █▀▀▀▀███████▄██▀ │" + style 0-0 dim style 22-37 fg=blue - style 58-58 dim - style 59-59 fg=bright-magenta bold -9| " │ █▄ ▀███ ▀███ │ " - style 1-1 dim + style 59-59 dim +9| "│ █▄ ▀███ ▀███ │" + style 0-0 dim style 22-36 fg=blue - style 58-58 dim -10| " │ ▀█▄ ▀█████ │ " - style 1-1 dim + style 59-59 dim +10| "│ ▀█▄ ▀█████ │" + style 0-0 dim style 23-36 fg=blue - style 58-58 dim -11| " │ ▀█▄▄ █▄▄▀███▄ │ " - style 1-1 dim + style 59-59 dim +11| "│ ▀█▄▄ █▄▄▀███▄ │" + style 0-0 dim style 23-36 fg=blue - style 58-58 dim -12| " │ ▀▀▀▀▀▀ │ " - style 1-1 dim + style 59-59 dim +12| "│ ▀▀▀▀▀▀ │" + style 0-0 dim style 25-34 fg=blue - style 58-58 dim -13| " │ │ " - style 1-1 dim - style 58-58 dim -14| " │ DeepSeek Harness │ " - style 1-1 dim + style 59-59 dim +13| "│ │" + style 0-0 dim + style 59-59 dim +14| "│ DeepSeek Harness │" + style 0-0 dim style 22-37 fg=blue bold - style 58-58 dim -15| " │ │ " - style 1-1 dim - style 58-58 dim -16| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " - style 1-1 dim - style 58-58 dim -17| " │ │ " - style 1-1 dim - style 58-58 dim -18| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体 │ " - style 1-1 dim - style 58-58 dim -19| " │ 验难免粗粝。 │ " - style 1-1 dim - style 58-58 dim -20| " │ │ " - style 1-1 dim - style 58-58 dim -21| " │ “如切如磋,如琢如磨。” │ " - style 1-1 dim - style 3-24 bold - style 58-58 dim -22| " │ │ " - style 1-1 dim - style 58-58 dim -23| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真 │ " - style 1-1 dim - style 58-58 dim -24| " │ 实使用中暴露的问题,也可能促使我们重新审视,甚至推翻已 │ " - style 1-1 dim - style 58-58 dim -25| " │ 有的设计。 │ " - style 1-1 dim - style 58-58 dim -26| " │ │ " - style 1-1 dim - style 58-58 dim -27| " │ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未 │ " - style 1-1 dim - style 58-58 dim -28| " ├────────────────────────────────────────────────────────┤ " - style 1-58 dim -29| " │ Enter 继续 │ " - style 1-1 dim + style 59-59 dim +15| "│ │" + style 0-0 dim + style 59-59 dim +16| "│ 感谢您愿意拨冗试用 DeepSeek Harness。 │" + style 0-0 dim + style 59-59 dim +17| "│ │" + style 0-0 dim + style 59-59 dim +18| "│ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验 │" + style 0-0 dim + style 59-59 dim +19| "│ 难免粗粝。 │" + style 0-0 dim + style 59-59 dim +20| "│ │" + style 0-0 dim + style 59-59 dim +21| "│ “如切如磋,如琢如磨。” │" + style 0-0 dim + style 2-23 bold + style 59-59 dim +22| "│ │" + style 0-0 dim + style 59-59 dim +23| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实 │" + style 0-0 dim + style 59-59 dim +24| "│ 使用中暴露的问题,也可能促使我们重新审视,甚至推翻已有的 │" + style 0-0 dim + style 59-59 dim +25| "│ 设计。 │" + style 0-0 dim + style 59-59 dim +26| "│ │" + style 0-0 dim + style 59-59 dim +27| "│ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能 │" + style 0-0 dim + style 59-59 dim +28| "├──────────────────────────────────────────────────────────┤" + style 0-59 dim +29| "│ Enter 继续 │" + style 0-0 dim style 24-34 fg=bright-magenta bold - style 58-58 dim -30| " │ ↑/↓ 滚动 ↓ │ " - style 1-1 dim + style 59-59 dim +30| "│ ↑/↓ 滚动 ↓ │" + style 0-0 dim style 24-35 dim - style 58-58 dim -31| " ╰────────────────────────────────────────────────────────╯ " - style 1-58 dim + style 59-59 dim +31| "╰──────────────────────────────────────────────────────────╯" + style 0-59 dim 32-33| diff --git a/apps/cli/tests/snapshots/tui-first-run-welcome/80-columns.expected.txt b/apps/cli/tests/snapshots/tui-first-run-welcome/80-columns.expected.txt index 887a00d0cb..9c3287e290 100644 --- a/apps/cli/tests/snapshots/tui-first-run-welcome/80-columns.expected.txt +++ b/apps/cli/tests/snapshots/tui-first-run-welcome/80-columns.expected.txt @@ -6,98 +6,96 @@ viewport 3| " DEEPSEEK HARNESS" style 1-8 fg=bright-magenta bold style 10-16 bold -4| " ╭────────────────────────────────────────────────────────────────────────────╮ " - style 1-78 dim -5| " │ ▄▄▄▄▄▄▄██▀ █▄ ▄ │ " - style 1-1 dim +4| "╭──────────────────────────────────────────────────────────────────────────────╮" + style 0-79 dim +5| "│ ▄▄▄▄▄▄▄██▀ █▄ ▄ │" + style 0-0 dim style 26-53 fg=blue - style 78-78 dim -6| " │ ▄███████████▄▄ ███▄▄████ │ " - style 1-1 dim + style 79-79 dim +6| "│ ▄███████████▄▄ ███▄▄████ │" + style 0-0 dim style 26-53 fg=blue - style 78-78 dim -7| "/│ ████████████████▄ ▀██████▀ │F" - style 0-0 fg=bright-magenta bold - style 1-1 dim + style 79-79 dim +7| "│ ████████████████▄ ▀██████▀ │" + style 0-0 dim style 26-52 fg=blue - style 78-78 dim - style 79-79 fg=bright-magenta bold -8| " │ ██▀▀▀▀▀████████████▄▄██▀ │ " - style 1-1 dim + style 79-79 dim +8| "│ ██▀▀▀▀▀████████████▄▄██▀ │" + style 0-0 dim style 28-51 fg=blue - style 78-78 dim -9| " │ ██ ▀█████▄ ▀█████ │ " - style 1-1 dim + style 79-79 dim +9| "│ ██ ▀█████▄ ▀█████ │" + style 0-0 dim style 28-50 fg=blue - style 78-78 dim -10| " │ ██▄ ▀████▄ ▄████ │ " - style 1-1 dim + style 79-79 dim +10| "│ ██▄ ▀████▄ ▄████ │" + style 0-0 dim style 28-50 fg=blue - style 78-78 dim -11| " │ ██▄ ████████▀ │ " - style 1-1 dim + style 79-79 dim +11| "│ ██▄ ████████▀ │" + style 0-0 dim style 29-50 fg=blue - style 78-78 dim -12| " │ ██▄ ▄▄ ▀█████▀ │ " - style 1-1 dim + style 79-79 dim +12| "│ ██▄ ▄▄ ▀█████▀ │" + style 0-0 dim style 29-49 fg=blue - style 78-78 dim -13| " │ ▀███▄▄▄███▄ ████▄▄ │ " - style 1-1 dim + style 79-79 dim +13| "│ ▀███▄▄▄███▄ ████▄▄ │" + style 0-0 dim style 29-50 fg=blue - style 78-78 dim -14| " │ ▀▀▀███████▀▀ │ " - style 1-1 dim + style 79-79 dim +14| "│ ▀▀▀███████▀▀ │" + style 0-0 dim style 31-47 fg=blue - style 78-78 dim -15| " │ │ " - style 1-1 dim - style 78-78 dim -16| " │ DeepSeek Harness │ " - style 1-1 dim + style 79-79 dim +15| "│ │" + style 0-0 dim + style 79-79 dim +16| "│ DeepSeek Harness │" + style 0-0 dim style 32-47 fg=blue bold - style 78-78 dim -17| " │ │ " - style 1-1 dim - style 78-78 dim -18| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ " - style 1-1 dim - style 78-78 dim -19| " │ │ " - style 1-1 dim - style 78-78 dim -20| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ " - style 1-1 dim - style 78-78 dim -21| " │ │ " - style 1-1 dim - style 78-78 dim -22| " │ “如切如磋,如琢如磨。” │ " - style 1-1 dim - style 3-24 bold - style 78-78 dim -23| " │ │ " - style 1-1 dim - style 78-78 dim -24| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题, │ " - style 1-1 dim - style 78-78 dim -25| " │ 也可能促使我们重新审视,甚至推翻已有的设计。 │ " - style 1-1 dim - style 78-78 dim -26| " │ │ " - style 1-1 dim - style 78-78 dim -27| " ├────────────────────────────────────────────────────────────────────────────┤ " - style 1-78 dim -28| " │ Enter 继续 │ " - style 1-1 dim + style 79-79 dim +17| "│ │" + style 0-0 dim + style 79-79 dim +18| "│ 感谢您愿意拨冗试用 DeepSeek Harness。 │" + style 0-0 dim + style 79-79 dim +19| "│ │" + style 0-0 dim + style 79-79 dim +20| "│ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │" + style 0-0 dim + style 79-79 dim +21| "│ │" + style 0-0 dim + style 79-79 dim +22| "│ “如切如磋,如琢如磨。” │" + style 0-0 dim + style 2-23 bold + style 79-79 dim +23| "│ │" + style 0-0 dim + style 79-79 dim +24| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也 │" + style 0-0 dim + style 79-79 dim +25| "│ 可能促使我们重新审视,甚至推翻已有的设计。 │" + style 0-0 dim + style 79-79 dim +26| "│ │" + style 0-0 dim + style 79-79 dim +27| "├──────────────────────────────────────────────────────────────────────────────┤" + style 0-79 dim +28| "│ Enter 继续 │" + style 0-0 dim style 34-44 fg=bright-magenta bold - style 78-78 dim -29| " │ ↑/↓ 滚动 ↓ │ " - style 1-1 dim + style 79-79 dim +29| "│ ↑/↓ 滚动 ↓ │" + style 0-0 dim style 34-45 dim - style 78-78 dim -30| " ╰────────────────────────────────────────────────────────────────────────────╯ " - style 1-78 dim + style 79-79 dim +30| "╰──────────────────────────────────────────────────────────────────────────────╯" + style 0-79 dim 31-32| diff --git a/apps/cli/tests/tui-first-run-welcome.spec.ts b/apps/cli/tests/tui-first-run-welcome.spec.ts index 6039c43640..44a345ba44 100644 --- a/apps/cli/tests/tui-first-run-welcome.spec.ts +++ b/apps/cli/tests/tui-first-run-welcome.spec.ts @@ -3,11 +3,14 @@ import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' +import type { Context } from 'cordis' 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 { acknowledgeTuiFirstRunWelcome, + apply, hasTuiFirstRunWelcomeAcknowledgement, + needsTuiFirstRunWelcomeAsciiArt, TuiFirstRunWelcomeComponent, tuiFirstRunWelcomeAcknowledgementPath, tuiFirstRunWelcomeArtTier, @@ -98,6 +101,16 @@ describe('TUI first-run welcome acknowledgement', () => { recursive: true, }) 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.join('\n')).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim()) 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', () => { @@ -140,6 +154,19 @@ describe('TUI first-run welcome composition', () => { const end = component.render(54).join('\n') expect(withoutWhitespace(end)).toContain(withoutWhitespace(copy.paragraphs.at(-1)!.slice(-10))) 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', () => { @@ -166,6 +193,23 @@ describe('TUI first-run welcome composition', () => { 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() + 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 () => { const fixture = hostFixture(30) let attempts = 0 @@ -186,4 +230,36 @@ describe('TUI first-run welcome composition', () => { expect(attempts).toBe(2) 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()) + }) }) diff --git a/apps/cli/tests/tui-keyless-smoke.e2e.ts b/apps/cli/tests/tui-keyless-smoke.e2e.ts index 80d58a498c..10e2c88e95 100644 --- a/apps/cli/tests/tui-keyless-smoke.e2e.ts +++ b/apps/cli/tests/tui-keyless-smoke.e2e.ts @@ -180,7 +180,9 @@ async function firstRunFrameSnapshot( const terminal = new HeadlessTerminal(columns, rows) try { 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 { await terminal.dispose() } @@ -206,11 +208,14 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => { tempDirPrefix: `dsh-tui-welcome-${String(columns)}-`, configPath: scriptedConfigPath, showFirstRunWelcome: true, + expectedExitCode: process.platform === 'win32' ? 0 : -15, columns, rows: 30, actions: [ { 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) => { 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`)) expect(output).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim()) expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`) - expect(output).toContain('\u001B[?2004l') }, LOADER_SMOKE_TEST_TIMEOUT_MS) 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-', configPath: scriptedConfigPath, showFirstRunWelcome: true, + expectedExitCode: process.platform === 'win32' ? 0 : -15, columns: 60, rows: 12, actions: [ { waitFor: firstRunCopy.paragraphs[0]!, send: '\x1b[F' }, { 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)) @@ -261,9 +266,10 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => { cwd, configPath: scriptedConfigPath, showFirstRunWelcome: true, + expectedExitCode: process.platform === 'win32' ? 0 : -15, actions: [ { 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) @@ -274,7 +280,8 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => { cwd, configPath: scriptedConfigPath, 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(`Enter ${firstRunCopy.continueLabel}`) @@ -283,6 +290,40 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => { } }, 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 () => { // With no configured welcome the borderless banner sweeps in left-to-right; // the detail line's session id (`main-session-`) renders only once @@ -462,6 +503,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => { binScript: dshBinScript, configArgs: ['--resume', 'resume-target', '--config', scriptedConfigPath], showFirstRunWelcome: true, + expectedExitCode: process.platform === 'win32' ? 0 : -15, prepare: async (cwd) => { await seedResumeSession(cwd) const before = await readFile(logPath( @@ -474,7 +516,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => { }, actions: [ { waitFor: firstRunCopy.paragraphs[0]!, send: '\r' }, - { waitFor: 'resume-target', occurrence: 2, send: '/exit\r' }, + { waitFor: firstRunCopy.saving, signal: 'SIGTERM', delayMs: 500 }, ], inspect: async (cwd) => { 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]) const appended = after.split('\n').filter(Boolean).slice(originalLineCount) .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: 'turn/start' })) },