Skip to content

Live word / character count indicator in the chat composer #60

Description

@DragonnZhang

Problem or Motivation

OpenWork's chat composer (FreeFormInput) gives no feedback about how much
you've written. The composer toolbar already surfaces a context-usage
indicator
(tokens used / total), a model picker, a thinking-level picker and an
attach control — but there is no length feedback for the draft itself. When
composing a long prompt (a spec, a big paste, a multi-paragraph instruction) you
have no glanceable sense of its size.

Comparable desktop composers and editors ship exactly this. The Codex desktop
composer and editor status bars (VS Code, Google Docs, the Claude/ChatGPT desktop
message boxes) all show a live word / character count of what you're
currently typing. It is a small, universally-expected affordance that OpenWork's
otherwise feature-rich composer toolbar is missing.

Proposed Solution

Add a small, unobtrusive draft count indicator to the composer toolbar
(left-of-center, next to the attach / working-directory badges, mirroring their
!compactMode rule):

  • Shows the word count of the current draft inline (e.g. Words: 12), only
    while the composer has non-whitespace text — so the empty composer stays clean.
  • A hover tooltip breaks down the full stats: Words, Characters, and
    Lines.
  • Counts update live on every keystroke (derived from the existing controlled
    input text — no extra state).
  • Hidden in compactMode (mirroring the context-usage / thinking / model
    controls) and hidden entirely when the draft is empty.

The counting itself (words / characters / lines) lives in a small pure,
unit-tested module
so its semantics (Unicode-aware character count,
whitespace-collapsing word count, newline-based line count) are locked down
without a DOM.

Feasibility

Classification: pure frontend — candidate. No qwen-code backend involvement.

  • The composer already stores the draft as a plain-text string in its controlled
    input state (FreeFormInput.tsx); the indicator is pure derived state over
    that value — no new IPC, no main-process change, no persistence.
  • The indicator reuses the existing composer-toolbar layout (it slots between the
    left badge group and the flex spacer) and the shared Tooltip primitives.
  • Three new i18n keys (chat.composerWords, chat.composerCharacters,
    chat.composerLines) added to all locales, using a plural-neutral
    label form
    ("Words: {{count}}") so no per-locale plural categories are
    needed.

Alternatives Considered

  • Character count only: simpler, but word count is the more meaningful metric
    for prose prompts; showing words inline and the full breakdown (words /
    characters / lines) on hover covers both without clutter.
  • A live token count of the draft: more useful in theory but requires a
    tokenizer that matches the backend model — that is backend-coupled and belongs
    with the existing context-usage indicator, not a pure-frontend counter.
  • Always-visible (even when empty): would clutter the empty composer, which
    the app deliberately keeps minimal; showing the indicator only when there's
    content matches the toolbar's other content-dependent affordances.

Additional Context

Feasibility verified against apps/electron: the composer's input is a plain
string (coerceInputText), the RichTextInput is a hand-rolled contentEditable
that fires onChange with plain text on every input event, and the toolbar's
bottom row already has a left badge group + flex spacer where the indicator
slots in cleanly.

Acceptance criteria (CDP e2e assertion)

A new e2e/assertions/composer-count.assert.ts drives the real built app over CDP
entirely in the draft (no-session) state — no seeded conversation and no backend:

  1. The composer's contenteditable renders and, while empty, the count
    indicator ([data-testid="composer-count"]) is absent — proving it stays
    hidden for an empty draft.
  2. Real typing (via CDP Input.insertText into the focused editor) of
    hello world makes the indicator appear with data-word-count="2" and
    data-char-count="11" — proving it reflects actual typed content.
  3. Appending more text updates the counts live (word count increases, char count
    increases) — proving it is reactive per keystroke, not a one-shot render.
  4. Selecting-all and deleting the draft makes the indicator disappear again —
    proving it reacts to the draft becoming empty.

In addition, the pure counting module ships with unit tests
(composer-count.test.ts) covering empty / whitespace-only input, single vs.
multiple words, collapsed internal whitespace, multi-line line counting, and
Unicode/emoji character counting.

Part of the autonomous desktop-feature loop (loop-bot).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions