Merge remote-tracking branch 'origin/master' into codex/experimental-cli-command-names

# Conflicts:
#	apps/cli/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
Turtle
2026-07-31 20:18:15 +08:00
58 changed files with 1939 additions and 209 deletions

View File

@@ -0,0 +1,111 @@
/**
* Static terminal rasters derived from the official 24x24 DeepSeek icon.
*
* Source: `../../assets/deepseek-color.svg`, whose path data is copied exactly
* from the supplied official icon (viewBox `0 0 24 24`, fill `#4D6BFE`). Each
* tier rasterizes that path into a square binary
* mask without redrawing its contour. The Unicode form packs two source rows
* into `▀`/`▄`/`█`; the ASCII fallback packs the same two bits into
* `'`/`_`/`#`. Assets contain no ANSI and are never generated at runtime.
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome-art
*/
/** Responsive official-icon raster tier. */
export type TuiFirstRunWelcomeArtTier = 'full' | 'compact' | 'minimal'
/** One raster with a block-cell primary and bit-equivalent ASCII fallback. */
export interface TuiFirstRunWelcomeArt {
/** Two vertical source pixels per terminal cell. */
readonly unicode: readonly string[]
/** Same two-bit cells encoded as top `'`, bottom `_`, and both `#`. */
readonly ascii: readonly string[]
}
const fullUnicode = Object.freeze([
' ▄',
' ▄▄▄▄▄▄▄▄▄▄███▀ ██▄',
' ▄███████████████▄ ████▄ ▄▄▄▄██',
' ▄███████████████████▄ ████████████▀',
' ▄██████████████████████▄ ▀█████████▀',
'▄███▀█████████████████████▄ ████▀▀',
'███ ▀▀█████████▀▀▀█████████▀',
'███ ▀███████▀█ ▀███████',
'███▄ ▀███████▄ ▀█████▀',
'▀███ ▀██████████████',
' ▀███▄ ▀███████████▀',
' ▀███▄ ▄▄▄ ▀████████▀',
' █████▄ ███▄▄ ▀█████▄▄',
' ▀█████████████▄▄▄▄█▀█████▀',
' ▀▀███████████▀▀',
])
const fullAscii = Object.freeze([
' _',
" __________###' ##_",
' _###############_ ####_ ____##',
" _###################_ ############'",
" _######################_ '#########'",
"_###'#####################_ ####''",
"### ''#########'''#########'",
"### '#######'# '#######",
"###_ '#######_ '#####'",
"'### '##############",
" '###_ '###########'",
" '###_ ___ '########'",
" #####_ ###__ '#####__",
" '#############____#'#####'",
" ''###########''",
])
const compactUnicode = Object.freeze([
' ▄▄▄▄▄▄▄██▀ █▄ ▄',
' ▄███████████▄▄ ███▄▄████',
' ████████████████▄ ▀██████▀',
'██▀▀▀▀▀████████████▄▄██▀',
'██ ▀█████▄ ▀█████',
'██▄ ▀████▄ ▄████',
' ██▄ ████████▀',
' ██▄ ▄▄ ▀█████▀',
' ▀███▄▄▄███▄ ████▄▄',
' ▀▀▀███████▀▀',
])
const compactAscii = Object.freeze([
" _______##' #_ _",
' _###########__ ###__####',
" ################_ '######'",
"##'''''############__##'",
"## '#####_ '#####",
"##_ '####_ _####",
" ##_ ########'",
" ##_ __ '#####'",
" '###___###_ ####__",
" '''#######''",
])
const minimalUnicode = Object.freeze([
' ▄▄▄▄▄▄ ▄▄',
' ▄████████▄ ▀████▀',
'█▀▀▀▀███████▄██▀',
'█▄ ▀███ ▀███',
'▀█▄ ▀█████',
' ▀█▄▄ █▄▄▀███▄',
' ▀▀▀▀▀▀',
])
const minimalAscii = Object.freeze([
' ______ __',
" _########_ '####'",
"#''''#######_##'",
"#_ '### '###",
"'#_ '#####",
" '#__ #__'###_",
" ''''''",
])
/** Exact-path terminal rasters by responsive tier. */
export const TUI_FIRST_RUN_WELCOME_WHALE = Object.freeze({
full: Object.freeze({ unicode: fullUnicode, ascii: fullAscii }),
compact: Object.freeze({ unicode: compactUnicode, ascii: compactAscii }),
minimal: Object.freeze({ unicode: minimalUnicode, ascii: minimalAscii }),
}) satisfies Readonly<Record<TuiFirstRunWelcomeArtTier, TuiFirstRunWelcomeArt>>

View File

@@ -0,0 +1,49 @@
/**
* Centrally owned version and all-locale Chinese copy for the shipped TUI first-run notice.
*
* A material wording change increments {@link TUI_FIRST_RUN_WELCOME_NOTICE_VERSION}
* so every Harness home presents the revised notice once.
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome-copy
*/
/** Copy version persisted after the user explicitly continues. */
export const TUI_FIRST_RUN_WELCOME_NOTICE_VERSION = 4
/** Locale-shaped text rendered by the first-run welcome overlay. */
export interface TuiFirstRunWelcomeNoticeCopy {
/** Overlay heading. */
readonly title: string
/** Ordered prose paragraphs. */
readonly paragraphs: readonly string[]
/** Enter action label. */
readonly continueLabel: string
/** Hint shown when the prose is scrollable. */
readonly scrollHint: string
/** Status shown while the acknowledgement reaches disk. */
readonly saving: string
/** Retry message shown when the acknowledgement cannot be persisted. */
readonly saveError: string
}
/** Complete Chinese notice used for every locale. */
const TUI_FIRST_RUN_WELCOME_CHINESE_COPY = Object.freeze<TuiFirstRunWelcomeNoticeCopy>({
title: 'DeepSeek Harness',
paragraphs: Object.freeze([
'感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能仍待完善体验难免有些粗糙。',
'“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。',
'为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log如需关闭请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外如果您有任何反馈与建议请在企业微信群中留言告诉我们。每一条反馈都会帮助我们把它打磨得更好。',
]),
continueLabel: '继续',
scrollHint: '↑/↓ 滚动',
saving: '正在保存确认…',
saveError: '无法保存确认,请按 Enter 重试。',
})
/** Locale map whose entries deliberately share the single Chinese owner copy. */
export const TUI_FIRST_RUN_WELCOME_NOTICE_COPY = Object.freeze({
'zh-CN': TUI_FIRST_RUN_WELCOME_CHINESE_COPY,
en: TUI_FIRST_RUN_WELCOME_CHINESE_COPY,
})
/** Locale presented by the shipped first-run notice. */
export const TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE = 'zh-CN' as const

View File

@@ -0,0 +1,385 @@
/**
* Effect-owned first-run overlay for the shipped `dsh` TUI.
*
* The launcher owns the per-DSH_HOME acknowledgement boundary; the component
* reaches the terminal only through the mounted `ctx.tui` overlay service and
* never touches the session or model context.
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome
*/
import { randomUUID } from 'node:crypto'
import { lstat, mkdir, open, rename, rm } from 'node:fs/promises'
import { basename, dirname, join } from 'node:path'
import type { Context } from 'cordis'
import {
Key,
matchesKey,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from '@earendil-works/pi-tui'
import {
disposeRootAndExit,
type TuiComponent,
type TuiFocusable,
type TuiOverlayHost,
} from '@deepseek-ai/dsh-tui'
import {
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
type TuiFirstRunWelcomeNoticeCopy,
} from './tui-first-run-welcome-copy.ts'
import {
TUI_FIRST_RUN_WELCOME_WHALE,
type TuiFirstRunWelcomeArtTier,
} from './tui-first-run-welcome-art.ts'
// TODO: Move acknowledgement persistence behind @deepseek-ai/dsh-storage once
// its backend contract supports concurrent host processes. This same-value
// marker must not inherit JSON lost updates or SQLite busy failures.
const ACKNOWLEDGEMENT_DIRECTORY = 'notices'
const ACKNOWLEDGEMENT_BASENAME = 'tui-first-run-welcome'
/** Cordis plugin name. */
export const name = 'tui-first-run-welcome'
/** The notice can open only after the terminal-local overlay service mounts. */
export const inject = ['tui']
/** Launcher-resolved configuration for the terminal-local notice. */
interface Config {
/** Absolute DeepSeek Harness home owning this acknowledgement. */
readonly dshHome: string
/** Render the bit-equivalent printable ASCII icon fallback. */
readonly asciiArt?: boolean
}
/**
* Detect an explicitly non-Unicode terminal locale for the static ASCII art fallback.
* @param env - Process environment carrying locale and terminal declarations.
* @returns `true` only when the environment explicitly declares an ASCII-only locale or dumb terminal.
*/
export function needsTuiFirstRunWelcomeAsciiArt(
env: Readonly<Record<string, string | undefined>> = process.env,
): boolean {
const locale = env.LC_ALL ?? env.LC_CTYPE ?? env.LANG
return env.TERM === 'dumb' || locale === 'C' || locale === 'POSIX'
}
/**
* Resolve the immutable marker for one notice version.
* @param dshHome - Resolved Harness home.
* @param version - Copy version whose acknowledgement is queried.
* @returns Absolute marker path beneath the Harness home.
*/
export function tuiFirstRunWelcomeAcknowledgementPath(dshHome: string, version: number): string {
return join(
dshHome,
ACKNOWLEDGEMENT_DIRECTORY,
`${ACKNOWLEDGEMENT_BASENAME}-v${String(version)}.ack`,
)
}
/**
* Test whether one notice version has been acknowledged.
* @param dshHome - Resolved Harness home.
* @param version - Copy version to inspect.
* @returns `true` only for a regular marker file; a malformed marker fails loud.
*/
export async function hasTuiFirstRunWelcomeAcknowledgement(
dshHome: string,
version: number = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
): Promise<boolean> {
const path = tuiFirstRunWelcomeAcknowledgementPath(dshHome, version)
try {
const info = await lstat(path)
if (!info.isFile()) throw new Error(`TUI welcome acknowledgement is not a file: ${path}`)
return true
} catch (error) {
if ((error as NodeJS.ErrnoException | null)?.code === 'ENOENT') return false
throw error
}
}
/**
* Persist one version acknowledgement by syncing a random same-directory file
* before atomically replacing the immutable marker. Concurrent launches publish
* the same fact, so same-value last-writer-wins replacement loses no state.
* @param dshHome - Resolved Harness home.
* @param version - Copy version being acknowledged.
*/
export async function acknowledgeTuiFirstRunWelcome(
dshHome: string,
version: number = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
): Promise<void> {
const path = tuiFirstRunWelcomeAcknowledgementPath(dshHome, version)
const directory = dirname(path)
const temp = join(directory, `.${basename(path)}.${randomUUID()}.tmp`)
await mkdir(directory, { recursive: true, mode: 0o700 })
await syncDirectory(dirname(directory))
let handle: Awaited<ReturnType<typeof open>> | undefined
try {
handle = await open(temp, 'wx', 0o600)
await handle.sync()
const created = handle
handle = undefined
await created.close()
await rename(temp, path)
} catch (error) {
/* v8 ignore start -- fault-injected UI coverage proves failed acknowledgements stay uncommitted and retryable */
try {
await handle?.close()
} finally {
await rm(temp, { force: true })
}
throw error
/* v8 ignore stop */
}
try {
await syncDirectory(directory)
/* v8 ignore next -- rename is the commit point; directory-fsync fault injection is platform-specific */
} catch {
// Swallow post-rename directory fsync failure: the marker is already committed,
// and crash loss can only make the notice reappear on the safe side.
}
}
/** Sync one POSIX directory after publishing a child entry. */
/* v8 ignore start -- Windows rejects directory opens; POSIX unit coverage owns this path. */
async function syncDirectory(path: string): Promise<void> {
if (process.platform === 'win32') return
const handle = await open(path, 'r')
try {
await handle.sync()
} finally {
await handle.close()
}
}
/* v8 ignore stop */
/** Render one visible-width-padded line inside the notice frame. */
function framed(content: string, innerWidth: number, host: TuiOverlayHost): string {
const clipped = truncateToWidth(content, innerWidth, '')
return `${host.theme.dim('│')} ${clipped}${' '.repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} ${host.theme.dim('│')}`
}
/** Center one line by terminal column width. */
function centered(content: string, width: number): string {
const clipped = truncateToWidth(content, width, '')
const remaining = Math.max(0, width - visibleWidth(clipped))
return `${' '.repeat(Math.floor(remaining / 2))}${clipped}`
}
/**
* Select the art tier for the actual overlay width and viewport height.
* @param innerWidth - Columns inside the frame.
* @param viewportRows - Current terminal rows.
* @returns full, compact, minimal, or no art when prose must take priority.
*/
export function tuiFirstRunWelcomeArtTier(
innerWidth: number,
viewportRows: number,
): TuiFirstRunWelcomeArtTier | undefined {
const compositionCapacity = Math.max(1, Math.max(7, Math.floor(viewportRows * 0.9)) - 5)
if (innerWidth >= 96 && TUI_FIRST_RUN_WELCOME_WHALE.full.unicode.length <= compositionCapacity) return 'full'
if (innerWidth >= 80 && TUI_FIRST_RUN_WELCOME_WHALE.compact.unicode.length + 4 <= compositionCapacity) return 'compact'
if (innerWidth >= 64 && TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode.length + 4 <= compositionCapacity) return 'minimal'
return undefined
}
/** Wrap the centrally owned prose while promoting its opening quotation. */
function proseLines(
copy: TuiFirstRunWelcomeNoticeCopy,
width: number,
host: TuiOverlayHost,
): string[] {
const lines: string[] = []
for (const [index, paragraph] of copy.paragraphs.entries()) {
if (index > 0) lines.push('')
const quoteEnd = paragraph.startsWith('“') ? paragraph.indexOf('”') : -1
if (quoteEnd > 0) {
const quote = paragraph.slice(0, quoteEnd + 1)
const remainder = paragraph.slice(quoteEnd + 1).trimStart()
lines.push(...wrapTextWithAnsi(host.theme.bold(host.theme.text(host.display(quote))), width))
lines.push('')
if (remainder !== '') lines.push(...wrapTextWithAnsi(host.theme.text(host.display(remainder)), width))
} else {
lines.push(...wrapTextWithAnsi(host.theme.text(host.display(paragraph)), width))
}
}
return lines
}
/** Render centered static brand art without putting ANSI into its owner file. */
function artLines(
tier: TuiFirstRunWelcomeArtTier,
width: number,
host: TuiOverlayHost,
asciiArt: boolean,
): string[] {
const art = TUI_FIRST_RUN_WELCOME_WHALE[tier][asciiArt ? 'ascii' : 'unicode']
return art.map(line => centered(host.theme.brand(line), width))
}
/** Responsive, scrollable notice whose only completion input is Enter. */
export class TuiFirstRunWelcomeComponent implements TuiComponent, TuiFocusable {
focused = false
private scrollOffset = 0
private bodyCapacity = 1
private maxScrollOffset = 0
private saving = false
private saveFailed = false
constructor(
private readonly host: TuiOverlayHost,
private readonly copy: TuiFirstRunWelcomeNoticeCopy,
private readonly acknowledge: () => Promise<void>,
private readonly exit: () => void,
private readonly asciiArt = false,
) {}
invalidate(): void {}
render(width: number): string[] {
const frameWidth = Math.max(6, width)
const innerWidth = Math.max(1, frameWidth - 4)
const viewportRows = this.host.viewport.rows
const tier = tuiFirstRunWelcomeArtTier(innerWidth, viewportRows)
const availableRows = Math.max(7, Math.floor(viewportRows * 0.9))
const title = this.host.theme.bold(this.host.theme.brand(this.copy.title))
let fixedHeader: string[] = []
let fullContentHeader: string[] = []
let body: string[]
let fullArt: string[] | undefined
const fullArtWidth = 44
if (tier === 'full') {
fullArt = artLines(tier, fullArtWidth, this.host, this.asciiArt)
const contentWidth = Math.max(1, innerWidth - fullArtWidth - 3)
fullContentHeader = [centered(title, contentWidth), '']
body = proseLines(this.copy, contentWidth, this.host)
} else {
const art = tier === undefined ? [] : artLines(tier, innerWidth, this.host, this.asciiArt)
fixedHeader = [...art, ...art.length === 0 ? [] : [''], centered(title, innerWidth), '']
body = proseLines(this.copy, innerWidth, this.host)
}
const compositionCapacity = Math.max(1, availableRows - 5)
const bodyLimit = Math.max(1, compositionCapacity - fixedHeader.length - fullContentHeader.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)
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 bottom = this.host.theme.dim(`${'─'.repeat(Math.max(0, frameWidth - 2))}`)
const action = this.host.theme.bold(this.host.theme.accent(`Enter ${this.copy.continueLabel}`))
const hasAbove = this.scrollOffset > 0
const hasBelow = this.scrollOffset < maxOffset
const scroll = hasAbove || hasBelow
? `${hasAbove ? '↑' : ' '} ${this.copy.scrollHint} ${hasBelow ? '↓' : ' '}`
: ''
const status = this.saveFailed
? this.host.theme.error(this.copy.saveError)
: this.saving
? this.host.theme.dim(this.copy.saving)
: this.host.theme.dim(scroll)
const fullContent = [...fullContentHeader, ...visibleBody]
const composition = fullArt === undefined
? [...fixedHeader, ...visibleBody]
: Array.from({ length: Math.max(fullArt.length, fullContent.length) }, (_, index) => {
const art = fullArt[index] ?? ''
const line = fullContent[index] ?? ''
const left = `${art}${' '.repeat(Math.max(0, fullArtWidth - visibleWidth(art)))}`
return `${left} ${line}`
})
return [
top,
...composition.map(line => framed(line, innerWidth, this.host)),
separator,
framed(centered(action, innerWidth), innerWidth, this.host),
framed(centered(status, innerWidth), innerWidth, this.host),
bottom,
]
}
handleInput(data: string): void {
if (matchesKey(data, Key.ctrl('c')) || matchesKey(data, Key.ctrl('d'))) {
this.exit()
return
}
if (matchesKey(data, Key.enter)) {
if (!this.saving) void this.commit()
return
}
if (this.saving || matchesKey(data, Key.escape)) return
if (matchesKey(data, Key.up)) this.scrollBy(-1)
else if (matchesKey(data, Key.down)) this.scrollBy(1)
else if (matchesKey(data, Key.pageUp)) this.scrollBy(-this.bodyCapacity)
else if (matchesKey(data, Key.pageDown)) this.scrollBy(this.bodyCapacity)
else if (matchesKey(data, Key.home)) this.scrollTo(0)
else if (matchesKey(data, Key.end)) this.scrollTo(this.maxScrollOffset)
}
private scrollBy(delta: number): void {
this.scrollTo(this.scrollOffset + delta)
}
private scrollTo(offset: number): void {
this.scrollOffset = Math.min(this.maxScrollOffset, Math.max(0, offset))
this.host.invalidate()
}
private async commit(): Promise<void> {
this.saving = true
this.saveFailed = false
this.host.invalidate()
try {
await this.acknowledge()
this.host.close()
} catch {
this.saving = false
this.saveFailed = true
this.host.invalidate()
}
}
}
/**
* Open the first-run notice through the mounted TUI's FIFO overlay owner.
* @param ctx - Plugin context carrying the terminal-local TUI service.
* @param config - Launcher-resolved Harness home.
*/
export function apply(ctx: Context, config: Config): void {
const copy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
const pending = new Set<Promise<void>>()
const acknowledge = (): Promise<void> => {
const task = acknowledgeTuiFirstRunWelcome(config.dshHome)
pending.add(task)
const settled = (): void => { pending.delete(task) }
void task.then(settled, settled)
return task
}
ctx.effect(() => async () => {
await Promise.allSettled(pending)
}, 'tui first-run welcome acknowledgement')
ctx.tui.openOverlay({
create: host => new TuiFirstRunWelcomeComponent(
host,
copy,
acknowledge,
() => { disposeRootAndExit(ctx, 0) },
config.asciiArt ?? false,
),
options: {
width: '100%',
maxHeight: '90%',
anchor: 'center',
margin: 0,
},
})
}

View File

@@ -29,6 +29,7 @@ import {
loadPersonalPatches,
resolveConfigPath,
} from '@deepseek-ai/dsh-app-boot'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { SessionId } from '@deepseek-ai/dsh-session'
import { configHasTelemetryRow, resolveTelemetryPatch } from './app-cli-entry.ts'
import { SESSION_QUERY_SQLITE_PATH_KEY } from '@deepseek-ai/dsh-session-query-sqlite'
@@ -41,6 +42,16 @@ import {
type MainSessionIdentity,
type TuiResumeHost,
} from '@deepseek-ai/dsh-tui'
import {
apply as applyTuiFirstRunWelcome,
hasTuiFirstRunWelcomeAcknowledgement,
inject as tuiFirstRunWelcomeInject,
name as tuiFirstRunWelcomeName,
needsTuiFirstRunWelcomeAsciiArt,
} from './tui-onboarding/tui-first-run-welcome.ts'
import {
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
} from './tui-onboarding/tui-first-run-welcome-copy.ts'
const NAME = 'dsh'
@@ -113,6 +124,11 @@ export async function runTui(
// both together. Sessions themselves live under the Harness home so `/resume`
// spans every workspace, and are unaffected by this chdir.
if (workspace !== undefined) process.chdir(workspace)
const dshHome = resolveDshHome()
const showFirstRunWelcome = !await hasTuiFirstRunWelcomeAcknowledgement(
dshHome,
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
)
process.env.DSH_BUNDLED_SKILL_DIR = join(SOURCE_ROOT, 'skills')
// The in-place `/resume` handoff re-execs `dsh` with a normalized `--resume`
// flag, so the resumed process rehydrates through this same intake. The
@@ -232,5 +248,15 @@ export async function runTui(
)
app.current = ctx
addHarnessSourceSection(ctx, SOURCE_ROOT)
if (showFirstRunWelcome) {
await ctx.plugin({
name: tuiFirstRunWelcomeName,
inject: tuiFirstRunWelcomeInject,
apply: applyTuiFirstRunWelcome,
}, {
dshHome,
asciiArt: needsTuiFirstRunWelcomeAsciiArt(),
})
}
}
/* v8 ignore stop */