docs: consolidate TUI Agent Notes
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-07-28-tui-palette-single-source.md
|
||||
2026-07-28-tui-palette-single-source.md: 7027e650bfb94dca7608342b10c994b255f48d2d
|
||||
2026-07-28-tui-palette-single-source.zh.md: a956a916c01feceedc7b6b6715bee6ceb8b80e7f
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-consolidated-tui-presentation.md
|
||||
2026-07-28-consolidated-tui-presentation.md: f87d543a698d6e77abf9120c6579100df4b60b64
|
||||
2026-07-28-consolidated-tui-presentation.zh.md: 005e408f0e75207027315546942f9eab57d595d1
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Note: Consolidated TUI presentation and navigation
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-consolidated-tui-presentation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The terminal UI accumulated independent presentation rules that interacted poorly: palette roles aliased one another or inverted emphasis on light terminals; tool-card framing, output, and exit markers repeated or competed; injected context was parsed as XML and could not fold reliably; and `/resume` excluded sessions outside the current workspace even when the launcher could reach them. Each symptom appeared local, but the durable decision is one terminal-reading model: a small inspectable palette, status-led cards with recessed bodies, content-independent transcript folding, and workspace-aware navigation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Palette
|
||||
|
||||
`paletteSpec(scheme)` is the single table of SGR codes, close codes, and purposes. `createPalette` derives every wrapper from it and `/palette` prints the same table in the running terminal. Components do not emit their own SGR sequences except for the fixed startup brand gradient. Every close resets every SGR group its open sets.
|
||||
|
||||
Duplicate roles are merged: `muted` into `dim`, `added` into `success`, `removed` into `error`, and the unused second accent is removed. `dim` uses `2;39` and closes with `22;39` on both schemes so recessed text stays relative to the terminal foreground rather than becoming a fixed heavy gray on light backgrounds. Colors and attributes are branded separately in TypeScript, allowing attribute/color composition while rejecting nested colors whose reset would discard the outer color.
|
||||
|
||||
### Tool cards
|
||||
|
||||
A tool card has one colored `Tool / <name>` status header over one dim body. Presenter titles, terminal commands and cwd rows, output, XML text, and fold markers use that body tone. Diff colors remain because red and green carry meaning, and signal markers remain errors.
|
||||
|
||||
`renderUnknownXml` receives an explicit body styler for unknown tool results. Terminal presenters parse and remove the model-facing final exit or signal marker before returning `TerminalResultView.output`; the TUI renders the structured status once as its own pill. Truncation, timeout, and sandbox lines remain in the body because the pill does not represent them.
|
||||
|
||||
### Injected context and folding
|
||||
|
||||
Injected context renders as prose in `ContextCardComponent`, not through the XML tree renderer. Exact matched outer `<system-reminder>` lines are stripped, but mismatched, unpaired, or inline tag-like text remains verbatim. Model-facing content is unchanged. Folding uses the shared `preview` helper after body assembly, so it depends only on row count, never parser success or payload characters.
|
||||
|
||||
`Ctrl+O` cycles collapsed, expanded, and hidden. Tool cards disappear in the hidden state together with their card-owned leading gap. Context cards participate in collapsed and expanded states but fall back to collapsed while tools are hidden, because injected instructions are not disposable tool traffic.
|
||||
|
||||
### Cross-workspace resume
|
||||
|
||||
The resume picker summarizes all records and owns a current-workspace/all-workspaces scope toggled with Tab. It defaults to the current workspace, adds workspace labels only in the broader scope, and refuses records without a cwd because there is no directory to enter.
|
||||
|
||||
`TuiResumeHost.handoff` receives the selected `SessionId` and the cwd re-read during preflight. The CLI changes directory before disposing the current app, so an unreachable directory fails while the terminal can still recover; `execve` then inherits the selected workspace. The launcher also supplies the exit message rather than asking the TUI to reconstruct launcher syntax.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep separate notes and local fixes for each visual symptom.** Rejected: the decisions share one reading hierarchy and repeatedly superseded each other. One owner makes the final palette, card, context, and navigation rules clear without requiring readers to reconstruct chronology.
|
||||
|
||||
**Keep aliases and enforce presentation by convention.** Rejected: aliases imply distinctions that do not exist, and nested color resets or incomplete SGR closes fail silently. A single table plus types makes the contract inspectable and mechanically checked.
|
||||
|
||||
**Retain framing/output color splits inside tool cards.** Rejected: real cards mixed default foreground, cyan commands, dim cwd, unstyled XML, and dim output. The status header already provides the scan anchor; one recessed body removes noise. Diff colors are the narrow semantic exception.
|
||||
|
||||
**Parse or repair injected context as XML.** Rejected: reminder frames are prompting conventions around arbitrary prose containing raw ampersands, comparisons, and placeholder angle brackets. Repairing or escaping it would either guess structure or alter model-visible text.
|
||||
|
||||
**Hide context cards with tool cards.** Rejected: context carries injected instructions, not recoverable execution detail. The hidden phase therefore removes only tool traffic.
|
||||
|
||||
**Keep resume restricted to one workspace or infer cwd after boot.** Rejected: the restriction forces manual relaunch, while restored header cwd does not control filesystem and shell resolution. The target directory must cross the host seam before process replacement.
|
||||
|
||||
**Drop the TUI exit pill or remove model-facing exit markers.** Rejected: the pill is the scannable UI status, while the text marker is the model's status signal. The presenter consumes the marker when constructing the structured view so both audiences receive one representation.
|
||||
|
||||
## Consequences
|
||||
|
||||
The transcript reads as colored status headers over recessed detail, context presentation is stable for arbitrary prose, and one shortcut controls transcript density. The public `TuiTheme.muted` role is removed; extensions use `dim`. The palette and `renderUnknownXml` contracts are stricter, adding small compile-time friction in exchange for preventing silent style loss.
|
||||
|
||||
Cross-workspace resume can move every path-resolving tool to another directory. A missing or inaccessible cwd prevents handoff. The broader picker also makes concurrent access to a shared session store easier to reach; cross-process session locking remains separate work.
|
||||
|
||||
The terminal presenter still treats a final output line exactly matching its exit-marker grammar as structured status, so a command that intentionally prints such a line can lose it from the card body. This residual is documented by `dsh-tool-bash`.
|
||||
|
||||
## Testing
|
||||
|
||||
TUI unit and keyless terminal snapshots cover palette enumeration, light/dark roles, legal and illegal style composition, uniformly dim card bodies, semantic diff colors, marker-free terminal output with one exit pill, prose-preserving context frames, content-independent folding, the three-state Ctrl+O cycle, model filtering, and both resume scopes. CLI handoff tests cover passing the re-read cwd and rejecting directory-entry failure before teardown. Tool-bash tests pin result-marker emission, parse, and stripping as one round trip.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Note: 统一的 TUI 呈现与导航
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-consolidated-tui-presentation.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
终端 UI 逐步积累了多套彼此干扰的呈现规则:调色板角色互为别名,或在浅色终端中颠倒强调层级;工具卡片的框架、输出和退出标记重复或争夺注意力;注入上下文被当作 XML 解析,无法可靠折叠;`/resume` 即使能通过启动器访问其他工作区,也会排除不属于当前工作区的会话。每个症状看似局部,但持久决策只有一个终端阅读模型:精简且可检查的调色板、以状态为首且正文内收的卡片、与内容无关的记录折叠,以及感知工作区的导航。
|
||||
|
||||
## Decision
|
||||
|
||||
### 调色板
|
||||
|
||||
`paletteSpec(scheme)` 是 SGR 开始码、结束码和用途的唯一表。`createPalette` 从该表派生所有包装器,`/palette` 在运行中的终端打印同一张表。除固定的启动品牌渐变外,组件不自行发出 SGR 序列。每个结束码都重置对应开始码设置的所有 SGR 组。
|
||||
|
||||
重复角色被合并:`muted` 并入 `dim`,`added` 并入 `success`,`removed` 并入 `error`,未使用的第二强调色被移除。`dim` 在两种配色方案中都使用 `2;39`,并以 `22;39` 结束,使内收文本相对于终端前景色变暗,而不会在浅色背景上变成固定的深灰色。TypeScript 分别标记颜色和属性,允许属性与颜色组合,同时拒绝会因重置而丢失外层颜色的嵌套颜色。
|
||||
|
||||
### 工具卡片
|
||||
|
||||
工具卡片由一行带颜色的 `Tool / <name>` 状态标题和一块统一的 dim 正文组成。呈现器标题、终端命令及 cwd 行、输出、XML 文本和折叠标记都使用正文色调。差异颜色继续保留,因为红绿承载语义;信号标记也继续作为错误显示。
|
||||
|
||||
`renderUnknownXml` 对未知工具结果显式接收正文样式器。终端呈现器在返回 `TerminalResultView.output` 前解析并移除面向模型的末尾退出或信号标记;TUI 只把结构化状态呈现一次。截断、超时和沙箱信息继续留在正文中,因为状态标记不表达这些事实。
|
||||
|
||||
### 注入上下文与折叠
|
||||
|
||||
注入上下文由 `ContextCardComponent` 按普通文本呈现,不经过 XML 树渲染器。仅移除精确配对的外层 `<system-reminder>` 行;不匹配、单边或正文内类似标签的文本都原样保留。面向模型的内容不变。折叠在正文组装完成后使用共享 `preview` 辅助函数,因此只取决于行数,不依赖解析是否成功或载荷包含哪些字符。
|
||||
|
||||
`Ctrl+O` 在折叠、展开和隐藏之间循环。隐藏状态会连同卡片自有的前导间距一起移除工具卡片。上下文卡片参与折叠和展开状态,但工具隐藏时回到折叠状态,因为注入指令不是可丢弃的工具流量。
|
||||
|
||||
### 跨工作区恢复
|
||||
|
||||
恢复选择器汇总所有记录,并维护可用 Tab 切换的当前工作区/所有工作区范围。默认范围是当前工作区;只有更宽范围才显示工作区标签。没有 cwd 的记录会被拒绝,因为没有可进入的目录。
|
||||
|
||||
`TuiResumeHost.handoff` 接收选中的 `SessionId` 和预检时重新读取的 cwd。CLI 在释放当前应用前切换目录,因此无法访问的目录会在终端仍可恢复时失败;随后 `execve` 继承所选工作区。退出提示也由启动器提供,而不是让 TUI 反推启动器命令语法。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**为每个视觉症状保留独立 Agent Note 和局部修复。** 否决:这些决策共享同一阅读层级,而且彼此多次取代。由一份记录统一拥有最终的调色板、卡片、上下文和导航规则,读者无需重建变更顺序。
|
||||
|
||||
**保留别名,并依靠约定执行呈现规则。** 否决:别名暗示并不存在的差异;嵌套颜色重置或不完整的 SGR 结束会静默失败。单一表格加类型约束使契约可检查且可机械验证。
|
||||
|
||||
**保留工具卡片内部的框架/输出颜色分层。** 否决:真实卡片会混用默认前景、青色命令、dim cwd、无样式 XML 和 dim 输出。状态标题已经提供扫描锚点;统一内收正文能消除噪声。差异颜色是狭窄的语义例外。
|
||||
|
||||
**把注入上下文继续解析或修复成 XML。** 否决:提醒框架只是包裹任意普通文本的提示约定,其中会包含原始 `&`、比较表达式和尖括号占位符。修复或转义要么猜测结构,要么改变模型可见文本。
|
||||
|
||||
**随工具卡片一起隐藏上下文卡片。** 否决:上下文承载注入指令,不是可恢复的执行细节。因此隐藏阶段只移除工具流量。
|
||||
|
||||
**把恢复限制在一个工作区,或在启动后推断 cwd。** 否决:前者迫使用户手动重启;后者恢复的会话头 cwd 并不控制文件系统和 shell 的路径解析。目标目录必须在进程替换前跨过主机接口。
|
||||
|
||||
**移除 TUI 退出状态标记,或移除面向模型的退出标记。** 否决:前者是便于扫描的 UI 状态,后者是模型的状态信号。呈现器在构造结构化视图时消费文本标记,使两类受众各看到一种表示。
|
||||
|
||||
## Consequences
|
||||
|
||||
记录现在表现为带颜色的状态标题和内收细节;上下文对任意普通文本都稳定呈现;一个快捷键控制记录密度。公共 `TuiTheme.muted` 角色被移除,扩展改用 `dim`。调色板和 `renderUnknownXml` 契约更严格,以少量编译期摩擦换取对静默样式丢失的防护。
|
||||
|
||||
跨工作区恢复会把所有依赖路径解析的工具移动到另一个目录。cwd 缺失或不可访问时不能交接。更宽的选择范围也使共享会话存储的并发访问更容易触达;跨进程会话锁仍是独立后续工作。
|
||||
|
||||
终端呈现器仍会把与退出标记语法完全一致的最后一行输出视为结构化状态,因此命令有意打印这种行时,卡片正文可能丢失该行。`dsh-tool-bash` 已记录这一残余限制。
|
||||
|
||||
## Testing
|
||||
|
||||
TUI 单元测试和无密钥终端快照覆盖调色板枚举、浅色/深色角色、合法与非法样式组合、统一 dim 卡片正文、保留语义的差异颜色、仅有一个退出状态且正文无标记、普通文本上下文框架、与内容无关的折叠、Ctrl+O 三态循环、模型过滤和两种恢复范围。CLI 交接测试覆盖传递重新读取的 cwd,并在释放前拒绝目录切换失败。tool-bash 测试把结果标记的生成、解析和移除固定为同一轮往返契约。
|
||||
@@ -1,57 +0,0 @@
|
||||
# Agent Note: One palette table, one role per meaning, printable by `/palette`
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-tui-palette-single-source.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The palette had fifteen roles built from escape codes written inline in `createPalette`, and three pairs of them resolved to the same SGR parameters: `added`/`success` were both `32`, `removed`/`error` both `31`, and on a light scheme `muted`/`dim` were both `90`. A reader picking `added` or `muted` believed they had chosen a distinct tone; they had chosen an alias. Nothing enumerated the roles, so no reader could compare them, and the codes existed only as literals at their use site.
|
||||
|
||||
That hid a real defect for as long as it took a user to notice it. `dim` deliberately substituted ANSI 90 on light schemes, on the recorded theory that "SGR 2 lightens text on a light background." But lightening is precisely what a recessed tone must do, and ANSI 90 is a fixed hue: on a light theme whose default foreground is a soft gray, bright black is *heavier* than ordinary text. Every surface the TUI called dim — tool-card bodies, the timing footer, injected context, dialog chrome — therefore rendered as the most prominent text on screen, inverting the one relationship the role exists to express. The role named `dim` was the least dim thing in the transcript.
|
||||
|
||||
Nothing prevented the inversion from being introduced, and nothing revealed it afterwards. There was no listing to compare tones in, and terminal captures report escape codes rather than rendered luminance, so reading `\x1b[90m` and concluding "gray, therefore recessed" is a mistake a reader repeats indefinitely without a rendered sample to check against.
|
||||
|
||||
A second hazard was latent in the same design. SGR has no color stack: a nested span's close emits `39` (default foreground), not the enclosing color, so wrapping colored text in a second color silently drops the outer color for the remainder of the line. Only the type `(text: string) => string` guarded that, which is to say nothing guarded it.
|
||||
|
||||
## Decision
|
||||
|
||||
`paletteSpec(scheme)` is the single table of every SGR code the TUI may emit. Each entry carries `open`, `close`, and a `purpose` string. `createPalette` derives its wrappers by iterating `COLOR_ROLES` and `ATTRIBUTE_ROLES` over that table, and the `/palette` command prints it, so a role cannot exist in the palette without appearing in the listing or the reverse. No component writes an escape sequence of its own. The startup banner's brand gradient stays the one deliberate exception, since fixed brand color is its point.
|
||||
|
||||
`close` MUST reset every SGR group `open` sets. This is stated on `ansi` because the roles that violated it did so silently: `dim` opened `2;39` while closing only `22`, leaking a foreground reset past its own span.
|
||||
|
||||
Roles that resolved to the same escape are merged rather than kept as aliases. `muted` folds into `dim`, `added` into `success`, and `removed` into `error`, leaving seven colors and five attributes. `accent` becomes ANSI 95, the tone this terminal actually reads as emphasis, and the single-use `accent2` is deleted rather than kept as a second accent.
|
||||
|
||||
`dim` is `2;39` closing `22;39` on both schemes. SGR 2 fades relative to whatever foreground the terminal is using, which is the only way to land *below* `text` on a light and a dark theme with one code; `39` pins the starting foreground to the default so the tone does not inherit a caller's color. Only `code` still varies by scheme, because ANSI 36 is genuinely hard to read on a light background.
|
||||
|
||||
Colors and attributes are separately typed. A `ColorRole` takes `Colorable` and returns `Colored`; an `AttributeRole` is generic and preserves its argument's type. So `bold(accent(x))` and `accent(bold(x))` both compile, `accent(error(x))` does not, and — because the brand survives an attribute layer — neither does `dim(bold(success(x)))`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep ANSI 90 for `dim` and adjust the surfaces that looked wrong.** Rejected: the surfaces were right and the tone was wrong. Every consumer of `dim` wanted the same thing, so the fix belongs in the one role, not in each caller.
|
||||
|
||||
**Plain SGR 2 without the `39`.** Considered; the user compared both rendered against their own background. Both read as genuinely dim, and `2` alone is the smaller change, but `2;39` guarantees the span starts from the default foreground rather than inheriting a caller's color, which matters wherever `dim` wraps content that may already be styled.
|
||||
|
||||
**Keep `muted` and `dim` as separate names against future differentiation.** Rejected: they resolved to one escape on one of the two supported schemes, and a name that sometimes aliases another is worse than one name. Reintroducing a second recessed tone is a new decision, to be made when a consumer needs it.
|
||||
|
||||
**Enforce the no-nested-color rule by convention and review.** Rejected: the rule is mechanically checkable and the failure is invisible — a dropped outer color looks like a rendering quirk, not a bug. The brands cost two type aliases and caught four deliberate violations in a compile check.
|
||||
|
||||
**A runtime guard that strips or rejects nested colors.** Rejected: it moves a statically decidable error to run time and would have to allocate on every styled span, in the render hot path.
|
||||
|
||||
**Print the palette to stdout from a script instead of a TUI command.** Rejected: the point is to see the tones the *running* TUI produces in the *user's* terminal, under the scheme it actually resolved. A script cannot report the live scheme, and this specific bug was invisible outside the real terminal.
|
||||
|
||||
## Consequences
|
||||
|
||||
The listing is now the fastest way to find a palette defect: `/palette` shows every role painted by its own code beside the SGR pair it reports, so a tone that misbehaves is visible next to its neighbours instead of inferred from escape numbers. It also reports the resolved scheme, which surfaced that scheme detection is not stable across launches — an unrelated bug this change makes observable but does not fix.
|
||||
|
||||
Four role names disappear from the palette, and `muted` leaves the public extension `TuiTheme`; an extension that wants a recessed tone uses `dim`. Merging the diff pair means `success` and `error` each carry two meanings, which is honest about there being one green and one red rather than implying a diff-specific palette.
|
||||
|
||||
The brands introduce a small friction: an array literal seeded with a colored string infers `Colored[]`, so two call sites now annotate `string[]` explicitly. That is the cost of the guarantee, and it appears at declaration sites rather than in styling expressions.
|
||||
|
||||
`accent` moving to 95 repaints twenty-four call sites, including Markdown headings and links, dialog borders, and the prompt. That is a visible change to surfaces beyond the reported defect, made because a single emphasis color is the point of the reduction.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/ui/tui/tests/tui.spec.ts` asserts `/palette` prints every name and `purpose` in `paletteSpec` and that each row carries the spec's own open code, so a role added to the table without a listing entry, or a listing that reports one code while rendering another, fails. Verified by construction: truncating the attribute loop makes the test fail rather than pass silently.
|
||||
|
||||
The scheme-detection test previously pinned `dim` changing between schemes, which is no longer true; it now pins scheme-independent `dim` alongside the `code` role that does vary. The blank-row and running-glyph tests carry the new SGR pairs. The no-nested-color rule is verified by compiling deliberate violations against the project's own tsconfig, where the four color-over-color expressions are rejected and the five legal attribute compositions are not.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Agent Note: 单一调色板表、每种含义一个角色,并可通过 `/palette` 打印
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-tui-palette-single-source.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
调色板原有十五个角色,其转义码直接写在 `createPalette` 中,其中三对角色解析为相同的 SGR 参数:`added`/`success` 都是 `32`,`removed`/`error` 都是 `31`,而在浅色配色方案中,`muted`/`dim` 都是 `90`。读者选择 `added` 或 `muted` 时,会以为自己选了不同的色调,实际上选到的只是别名。没有任何地方枚举这些角色,因此读者无法比较它们,而这些转义码也仅以字面量存在于使用处。
|
||||
|
||||
这个设计一直掩盖着一个真实缺陷,直到用户亲眼发现。`dim` 在浅色配色方案中刻意改用 ANSI 90,依据是记录中「SGR 2(dim)会让浅色背景上的文本变淡」的理由。但变淡恰恰是弱化色调必须做的事,而 ANSI 90 是固定色相:如果浅色主题的默认前景色是柔和的灰色,亮黑色就会比普通文本*更重*。因此,TUI 中所有称作 dim 的界面——工具卡片正文、计时页脚、注入的上下文、对话框边框——都会渲染为屏幕上最醒目的文本,颠倒了该角色本应表达的唯一关系。名为 `dim` 的角色反而成了 transcript(文本记录)中最不弱化的内容。
|
||||
|
||||
这个颠倒既没有防范机制,发生后也没有显现机制。没有可供比较色调的清单,而终端捕获记录的是转义码而非渲染亮度,因此看到 `\x1b[90m` 就断定「这是灰色,所以会弱化」,是读者在没有渲染样例可供核对时会不断重复的错误。
|
||||
|
||||
同一设计中还潜藏着第二个风险。SGR 没有颜色栈:嵌套区段的关闭码会发出 `39`(默认前景色),而不是外层颜色,因此用第二种颜色包裹已经着色的文本,会悄然丢弃该行剩余部分的外层颜色。唯一的防护只是类型 `(text: string) => string`,也就是说实际上毫无防护。
|
||||
|
||||
## Decision
|
||||
|
||||
`paletteSpec(scheme)` 是 TUI 可以发出的所有 SGR 码的唯一表。每个条目都包含 `open`、`close` 和一个 `purpose` 字符串。`createPalette` 遍历该表的 `COLOR_ROLES` 与 `ATTRIBUTE_ROLES` 来派生包装函数,`/palette` 命令则打印该表,因此一个角色不可能只存在于调色板中却不出现在清单里,反之亦然。任何组件都不会自行写入转义序列。启动横幅的品牌渐变色是唯一的有意例外,因为固定品牌色本来就是它的目的。
|
||||
|
||||
`close` 必须重置 `open` 设置的每一个 SGR 组。该要求在 `ansi` 上明确说明,因为此前违反要求的角色没有显露问题:`dim` 以 `2;39` 开启,却只以 `22` 关闭,导致前景色重置越过其自身区段而泄漏。
|
||||
|
||||
解析为同一转义码的角色会合并,而不是作为别名保留。`muted` 并入 `dim`,`added` 并入 `success`,`removed` 并入 `error`,最终留下七种颜色和五种属性。`accent` 改为 ANSI 95,这是该终端中实际呈现为强调色的色调;仅使用一次的 `accent2` 则直接删除,不再作为第二种强调色保留。
|
||||
|
||||
在两种配色方案中,`dim` 都以 `2;39` 开启、以 `22;39` 关闭。SGR 2 会相对于终端当前使用的前景色减淡,这是仅用一个代码就在浅色和深色主题中落到 `text` *之下*的唯一方式;`39` 将起始前景色固定为默认值,使该色调不会继承调用方的颜色。只有 `code` 仍然随配色方案而变化,因为 ANSI 36 在浅色背景上确实难以辨认。
|
||||
|
||||
颜色与属性分别采用不同的类型。`ColorRole` 接受 `Colorable` 并返回 `Colored`;`AttributeRole` 是泛型,会保留实参的类型。因此 `bold(accent(x))` 和 `accent(bold(x))` 都能编译,`accent(error(x))` 不能;而且——由于品牌标记可以穿过一层属性——`dim(bold(success(x)))` 同样不能编译。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留 ANSI 90 作为 `dim`,并调整显示异常的界面。**否决:界面没有问题,错的是色调。`dim` 的所有消费方都需要同一种效果,因此修复应落在这个角色本身,而不是每个调用方中。
|
||||
|
||||
**不带 `39` 的纯 SGR 2。**考虑过;用户在自己的背景上比较了两者的渲染效果。两者看起来都确实弱化,单独使用 `2` 也是更小的改动,但 `2;39` 可以保证区段从默认前景色开始,而不是继承调用方的颜色;当 `dim` 包裹可能已有样式的内容时,这一点很重要。
|
||||
|
||||
**保留 `muted` 与 `dim` 两个名称,以备日后加以区分。**否决:它们在两种受支持的配色方案之一中解析为同一个转义码,而一个有时成为其他名称别名的名称,还不如只保留一个名称。重新引入第二种弱化色调是一项新决策,应等到消费方确有需要时再作出。
|
||||
|
||||
**通过约定和评审来执行禁止嵌套颜色的规则。**否决:这条规则可以机械检查,而且故障不可见——外层颜色丢失看起来会像渲染异常,而不是缺陷。品牌类型的代价只是两个类型别名,却在编译检查中捕获了四处刻意构造的违规。
|
||||
|
||||
**在运行时剥离或拒绝嵌套颜色的防护。**否决:这会把一个可静态判定的错误推迟到运行时,并且必须在渲染热路径中为每个带样式的区段分配内存。
|
||||
|
||||
**通过脚本将调色板打印到 stdout,而不是使用 TUI 命令。**否决:目的是在*运行中的* TUI 所实际解析出的配色方案下,查看它在*用户自己的*终端中产生的色调。脚本无法报告实时配色方案,而这个特定缺陷在真实终端之外不可见。
|
||||
|
||||
## Consequences
|
||||
|
||||
现在,清单是发现调色板缺陷最快的方式:`/palette` 会用每个角色自身的代码绘制该角色,并在旁边显示其报告的 SGR 码对,因此表现异常的色调会直接出现在邻近角色旁边,而不必根据转义码数字推断。它还会报告解析出的配色方案,由此揭示配色方案检测在多次启动之间并不稳定——这是本次变更令其可观察、但并未修复的无关缺陷。
|
||||
|
||||
调色板中消失了四个角色名称,`muted` 也从公共扩展 `TuiTheme` 中移除;需要弱化色调的扩展应使用 `dim`。合并差异对意味着 `success` 和 `error` 各自承载两种含义,这如实反映了系统只有一种绿色和一种红色,而不会暗示另有差异专用调色板。
|
||||
|
||||
品牌类型带来了一点不便:如果数组字面量以着色字符串开头,便会推断为 `Colored[]`,因此现在有两个调用点需要显式标注 `string[]`。这是该保证的代价,而且它出现在声明位置,而不是样式表达式中。
|
||||
|
||||
`accent` 改为 95 后,二十四个调用点随之重新着色,包括 Markdown 标题和链接、对话框边框以及提示行。这是报告缺陷之外其他界面的可见变化;作出这一改动,是因为精简的目的正是只保留一种强调色。
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/ui/tui/tests/tui.spec.ts` 断言 `/palette` 会打印每个名称和 `purpose`(均来自 `paletteSpec`),并且每一行都包含规格自身的开启码,因此在表中添加角色却没有清单条目,或清单报告的代码与渲染使用的代码不同,都会导致测试失败。构造验证也已完成:截断属性循环会让测试失败,而不会静默通过。
|
||||
|
||||
配色方案检测测试此前固定了 `dim` 会随配色方案变化的行为,但现在已不再如此;该测试现在同时固定与配色方案无关的 `dim`,以及确实会变化的 `code` 角色。空白行测试与运行状态字形测试包含新的 SGR 码对。禁止嵌套颜色的规则通过项目自身的 tsconfig 编译刻意构造的违规来验证,其中四个颜色叠加颜色的表达式会被拒绝,五个合法的属性组合则不会。
|
||||
Reference in New Issue
Block a user