Skip to content

feat(polish): cross-mode universal directives field#376

Open
katanumahotori wants to merge 3 commits into
Open-Less:betafrom
katanumahotori:feat/polish-universal-directives
Open

feat(polish): cross-mode universal directives field#376
katanumahotori wants to merge 3 commits into
Open-Less:betafrom
katanumahotori:feat/polish-universal-directives

Conversation

@katanumahotori
Copy link
Copy Markdown
Contributor

@katanumahotori katanumahotori commented May 9, 2026

User description

Why

Style modes (Light / Structured / Formal) describe voice — how verbose, how rewriting-friendly, how formal. They do not (and should not) encode user-specific typography conventions, which the same user wants applied across every mode.

Today a Japanese user who insists on full-width punctuation (、。 instead of ,.) and full-width spaces after !? has no way to express that without forking each of the four mode prompts. Same for English users who want the Oxford comma, Chinese users who want full-width paragraph indents, Korean users who want specific particle conventions, etc.

The dictionary covers vocabulary biasing, but not output formatting rules. There is no other prompt knob that survives a mode switch. This PR adds one.

What

A single new String preference, polish_universal_directives (default empty), whose contents are injected into every polish system prompt right after the mode-specific instructions and before the hotword block. Empty / whitespace values are no-ops — behavior is byte-for-byte unchanged for users who never touch the field.

Backend

  • types.rs — adds the field on UserPreferences with #[serde(default)] on UserPreferencesWire, so older preferences.json files deserialize cleanly. No migration needed.
  • polish.rscompose_system_prompt takes a third arg. When non-blank, it is appended with a Chinese section header consistent with the existing hotword block (通用规则(不论 polish mode 如何,始终遵守,与上述模式指示并存)). Order: mode prompt → universal rules → hotwords. Mode establishes voice; universal rules layer typography on top; hotwords are last (most specific, easiest for the model to attend to).
  • polish.rspolish() and translate_to() both accept the new arg. Translation deliberately receives it too: a user who wants full-width punctuation in their dictation also wants it in their translated output. Skipping translation would have produced confusing inconsistency.
  • coordinator.rspolish_or_passthrough, polish_text, translate_or_passthrough, translate_text, and the repolish command all thread the directive through. Read once from prefs.polish_universal_directives at session start.
  • commands.rsvalidate_llm_provider (the test-LLM-endpoint call) passes an empty string; connectivity validation does not need typography rules.

Frontend

  • Style.tsx — multi-line textarea at the top of the Style page, above the existing 4-mode grid. Persists on blur (no IPC per keystroke). Shares the master-toggle save-error / rollback path.
  • TS types & fixturesUserPreferences interface, defaultUserPrefs in ipc.ts, the test fixture in stylePrefs.test.ts.
  • i18n — five locales (ja / en / ko / zh-CN / zh-TW) with label, description, and a culturally appropriate placeholder example for each language's typography concerns.

Tests

Four new tests in polish::tests:

case expectation
empty / blank directives no 通用规则 block emitted (legacy parity)
non-empty directives section appended; user content reproduced verbatim
order contract 通用规则 appears before 热词 in the composed prompt
whitespace handling leading / trailing trimmed; interior newlines preserved

The pre-existing compose_system_prompt_prefers_correct_spelling_for_hotwords test is updated to pass \"\" for the new arg, exercising the no-op path.

Backwards compatibility

  • Default value is empty string → no change to the prompt body for any existing user.
  • #[serde(default)] means missing keys in older preferences.json deserialize without error.
  • Existing tests other than the one signature update continue to pass unchanged.
  • No new dependencies.

Out of scope

  • Raw mode bypasses the LLM, so directives have no effect there. This is intentional and documented in both the field's Rust doc-comment and the i18n description.
  • QA chat (answer_chat_streaming) and other non-polish/non-translate LLM paths are intentionally untouched — those are conversational, not output-formatting, surfaces. Adding directives there would risk turning conversational answers into stilted typography exercises.

PR Type

Enhancement, Tests


Description

  • Add polish_universal_directives field to UserPreferences

    • Stores cross‑mode typography / style rules set by the user
    • Empty string keeps legacy behaviour unchanged
  • Inject directives into polish and translation system prompts

    • compose_system_prompt appends them after mode instructions, before hotwords
    • translate_to appends them so translated output follows the same rules
  • Thread the new field through all coordinator polish/translate call chains

    • Updated polish_text, translate_text, polish_or_passthrough, translate_or_passthrough
  • Add Style page UI with a textarea and i18n strings for five locales

    • Local state avoids IPC per keystroke; commit happens on blur

Diagram Walkthrough

flowchart LR
  A["User enters directives in Style page"] -- "saves to" --> B["UserPreferences.polish_universal_directives"]
  B -- "retrieved by coordinator" --> C["polish_text / translate_text"]
  C -- "passed to" --> D["compose_system_prompt / translate_to"]
  D -- "appends after mode, before hotwords" --> E["LLM system prompt"]
  D -- "appends to translation prompt" --> F["LLM translate prompt"]
Loading

File Walkthrough

Relevant files
Enhancement
5 files
types.rs
Add `polish_universal_directives` field to UserPreferences and wire
+14/-0   
polish.rs
Inject universal directives into system prompt, add tests
+96/-6   
coordinator.rs
Pass directives through all polish/translate call chains 
+17/-0   
Style.tsx
Add universal directives textarea UI with blur commit       
+60/-0   
types.ts
Add polishUniversalDirectives to TypeScript UserPreferences interface
+4/-0     
Tests
3 files
commands.rs
Update test call to include empty directives                         
+1/-0     
ipc.ts
Update mock settings with new field default                           
+1/-0     
stylePrefs.test.ts
Add `polishUniversalDirectives: ''` to test mock preferences
+1/-0     
Documentation
5 files
en.ts
Add English UI labels for universal directives                     
+3/-0     
ja.ts
Add Japanese UI labels for universal directives                   
+3/-0     
ko.ts
Add Korean UI labels for universal directives                       
+3/-0     
zh-CN.ts
Add Simplified Chinese UI labels for universal directives
+3/-0     
zh-TW.ts
Add Traditional Chinese UI labels for universal directives
+3/-0     

Adds a single multi-line text field — `polish_universal_directives` on
UserPreferences — whose contents are injected into every polish (and
translate) system prompt, regardless of which polish mode the user has
selected. Empty / whitespace-only values are no-ops, so the change is
behavior-preserving by default.

## Why

Style modes (Light / Structured / Formal) describe *voice* — how
verbose, how rewriting-friendly, how formal. They do not (and should
not) encode user-specific *typography* conventions, which the same user
wants applied across every mode. Today a Japanese user who insists on
full-width punctuation has no way to express that without forking each
of the four mode prompts. Same for English users who want the Oxford
comma, Chinese users who want full-width paragraph indents, etc.

The dictionary covers vocabulary biasing, but not output formatting
rules. There is no other prompt knob that survives a mode switch. This
field fills that gap.

## What

- **`types.rs`** — adds `polish_universal_directives: String`
  (default empty) on `UserPreferences`; mirrored on `UserPreferencesWire`
  with `#[serde(default)]` so older preferences.json deserializes
  cleanly. No migration needed.
- **`polish.rs`** — `compose_system_prompt` takes a third arg
  `universal_directives: &str`. When non-blank, it is appended after the
  mode prompt and before the hotword block, with a Chinese-language
  section header consistent with the existing hotword block ("通用规则
  (不论 polish mode 如何,始终遵守,与上述模式指示并存)"). The order matters: mode
  prompt establishes voice → universal rules layer typography on top →
  hotwords are last (most specific, easiest to attend to).
- **`polish.rs`** — `polish()` and `translate_to()` both accept the new
  arg. Translation gets it too because the user wants their typography
  conventions in translated output as well, not just polished input.
- **`coordinator.rs`** — `polish_or_passthrough`, `polish_text`,
  `translate_or_passthrough`, `translate_text`, and the `repolish`
  command all thread the directive through. Read once from
  `prefs.polish_universal_directives` at session start; cloned String
  passed by ref through the chain.
- **`commands.rs`** — `validate_llm_provider` (the "test the LLM
  endpoint" call) passes an empty string; the validator does not need
  user typography rules to verify connectivity.
- **Frontend (`Style.tsx`)** — multi-line textarea added at the top of
  the Style page, above the existing 4-mode grid. Persists on `blur`
  to avoid 1 IPC per keystroke. Shares the same save-error /
  rollback path as the master toggle.
- **Types & i18n** — `UserPreferences` TS type, `defaultUserPrefs`
  fixtures (ipc.ts, stylePrefs.test.ts), and 5 locales (ja / en / ko /
  zh-CN / zh-TW) with label, description, and a placeholder example
  appropriate to that language's typography concerns.

## Tests

Four new tests in `polish::tests`:

- empty / blank directives produce no `通用规则` block (legacy parity)
- non-empty directives append the section, content reproduced verbatim
- order contract: directives appear before hotwords in the composed
  prompt (`directives_pos < hotwords_pos`)
- leading / trailing whitespace is trimmed without dropping interior
  newlines

The pre-existing
`compose_system_prompt_prefers_correct_spelling_for_hotwords` test is
updated to pass `""` for the new arg, exercising the no-op path.

## Out of scope

- Raw mode bypasses the LLM entirely, so directives have no effect there
  (documented in the field doc-comment and in the i18n description).
- QA chat (`answer_chat_streaming`) and other non-polish/non-translate
  LLM paths are intentionally untouched — those are conversational, not
  output-formatting, surfaces.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

PR Reviewer Guide 🔍

(Review updated until commit 05a2683)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

lightnovel0 added 2 commits May 9, 2026 12:46
The pre-existing `chat_completion_omits_authorization_when_api_key_is_empty`
integration test calls `provider.polish(...)` directly. Updated to pass
`""` for the new `universal_directives` parameter; this was missed in the
initial commit because the test file was below the search radius. CI
caught it cleanly on macOS / Windows / Linux.
…t-in prompt collision

The Chinese-language built-in mode prompts already contain a `# 通用规则`
(common rules) section. Naming the new universal-directives section
header `通用规则(...)` collided on the substring, which made
`!prompt.contains("通用规则")` in the new "empty directives = legacy
parity" test fail on every CI runner — the built-in section is always
present.

Renamed the new section header to `用户全局指令(...)` so it is unique to
this feature and unambiguously distinguishable from any built-in
section. Updated the three new tests to assert against the new keyword,
and tightened the order test's hotword anchor to `热词(` (the start of
the hotword block) rather than the substring `热词` (which appears in
discussion text inside the built-in prompt). Translate path renamed
in lockstep so polish and translate share the same marker.

User-facing strings (i18n, doc-comments) did not reference the Chinese
header text and need no changes.

Caught by upstream CI on macOS / Linux / (Windows pending). The local
test runner crashes with STATUS_ENTRYPOINT_NOT_FOUND on this
contributor's machine, so this regression had to be caught at PR push
time.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Persistent review updated to latest commit 8c19eb0

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Persistent review updated to latest commit 05a2683

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant