diff --git a/apps/electron/src/renderer/components/app-shell/input/ComposerCountIndicator.tsx b/apps/electron/src/renderer/components/app-shell/input/ComposerCountIndicator.tsx
new file mode 100644
index 000000000..4a5ae584c
--- /dev/null
+++ b/apps/electron/src/renderer/components/app-shell/input/ComposerCountIndicator.tsx
@@ -0,0 +1,54 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@craft-agent/ui'
+import { computeComposerCounts, shouldShowComposerCount } from '@/lib/composer-count'
+
+export interface ComposerCountIndicatorProps {
+ /** Current plain-text draft in the composer. */
+ text: string
+}
+
+/**
+ * ComposerCountIndicator — a subtle live word/character/line count for the
+ * composer draft.
+ *
+ * Renders `null` for an empty/whitespace-only draft so the empty composer stays
+ * clean. Shows the word count inline; the full breakdown (words / characters /
+ * lines) is in the hover tooltip. All values are exposed as `data-*` attributes
+ * for e2e assertions.
+ */
+export function ComposerCountIndicator({ text }: ComposerCountIndicatorProps) {
+ const { t } = useTranslation()
+
+ const counts = React.useMemo(() => computeComposerCounts(text), [text])
+
+ if (!shouldShowComposerCount(text)) return null
+
+ const wordsLabel = t('chat.composerWords', { count: counts.words })
+ const charactersLabel = t('chat.composerCharacters', { count: counts.characters })
+ const linesLabel = t('chat.composerLines', { count: counts.lines })
+
+ return (
+
+
+
+ {wordsLabel}
+
+
+
+
+ {wordsLabel}
+ {charactersLabel}
+ {linesLabel}
+
+
+
+ )
+}
diff --git a/apps/electron/src/renderer/components/app-shell/input/FreeFormInput.tsx b/apps/electron/src/renderer/components/app-shell/input/FreeFormInput.tsx
index bb596ed1d..fc8147ec9 100644
--- a/apps/electron/src/renderer/components/app-shell/input/FreeFormInput.tsx
+++ b/apps/electron/src/renderer/components/app-shell/input/FreeFormInput.tsx
@@ -71,6 +71,7 @@ import { resolveEffectiveConnectionSlug } from '@config/llm-connections';
import { useOptionalAppShellContext } from '@/context/AppShellContext';
import { EditPopover, getEditConfig } from '@/components/ui/EditPopover';
import { FreeFormInputContextBadge } from './FreeFormInputContextBadge';
+import { ComposerCountIndicator } from './ComposerCountIndicator';
import type {
AvailableSlashCommand,
FileAttachment,
@@ -2549,6 +2550,9 @@ export function FreeFormInput({
)}
+ {/* 3. Draft word/character count - hidden in compact mode and when empty */}
+ {!compactMode && }
+
{/* Spacer */}
diff --git a/apps/electron/src/renderer/lib/__tests__/composer-count.test.ts b/apps/electron/src/renderer/lib/__tests__/composer-count.test.ts
new file mode 100644
index 000000000..43a1b1852
--- /dev/null
+++ b/apps/electron/src/renderer/lib/__tests__/composer-count.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, it } from 'bun:test'
+import { computeComposerCounts, shouldShowComposerCount } from '../composer-count'
+
+describe('computeComposerCounts', () => {
+ it('returns zeros for an empty string', () => {
+ expect(computeComposerCounts('')).toEqual({ words: 0, characters: 0, lines: 0 })
+ })
+
+ it('treats whitespace-only text as zero words', () => {
+ expect(computeComposerCounts(' \n\t ')).toMatchObject({ words: 0 })
+ })
+
+ it('counts a single word', () => {
+ expect(computeComposerCounts('hello')).toEqual({ words: 1, characters: 5, lines: 1 })
+ })
+
+ it('counts multiple words and includes whitespace in the character count', () => {
+ expect(computeComposerCounts('hello world')).toEqual({
+ words: 2,
+ characters: 11,
+ lines: 1,
+ })
+ })
+
+ it('collapses runs of internal whitespace when counting words', () => {
+ expect(computeComposerCounts(' hello there\tworld ')).toMatchObject({ words: 3 })
+ })
+
+ it('ignores leading/trailing whitespace for word count but keeps it for characters', () => {
+ const counts = computeComposerCounts(' hi ')
+ expect(counts.words).toBe(1)
+ expect(counts.characters).toBe(6)
+ })
+
+ it('counts newline-delimited lines', () => {
+ expect(computeComposerCounts('a\nb\nc')).toMatchObject({ lines: 3 })
+ expect(computeComposerCounts('a\n')).toMatchObject({ lines: 2 })
+ })
+
+ it('counts astral characters (emoji) as one character each', () => {
+ // Two emoji + a space => 3 code points, 2 "words".
+ expect(computeComposerCounts('😀 🎉')).toEqual({ words: 2, characters: 3, lines: 1 })
+ })
+})
+
+describe('shouldShowComposerCount', () => {
+ it('is false for empty or whitespace-only drafts', () => {
+ expect(shouldShowComposerCount('')).toBe(false)
+ expect(shouldShowComposerCount(' \n\t')).toBe(false)
+ })
+
+ it('is true once there is any non-whitespace content', () => {
+ expect(shouldShowComposerCount('x')).toBe(true)
+ expect(shouldShowComposerCount(' hi ')).toBe(true)
+ })
+})
diff --git a/apps/electron/src/renderer/lib/composer-count.ts b/apps/electron/src/renderer/lib/composer-count.ts
new file mode 100644
index 000000000..55d195c68
--- /dev/null
+++ b/apps/electron/src/renderer/lib/composer-count.ts
@@ -0,0 +1,44 @@
+/**
+ * Pure helpers for the composer's live draft count indicator.
+ *
+ * Kept DOM-free so the counting semantics (Unicode-aware character count,
+ * whitespace-collapsing word count, newline-based line count) can be locked
+ * down with unit tests. The composer stores its draft as a plain-text string
+ * (see `coerceInputText`), so these operate directly on that value.
+ */
+
+export interface ComposerCounts {
+ /** Whitespace-separated word tokens (0 for empty / whitespace-only text). */
+ words: number
+ /** Unicode code points, so astral characters (emoji) count as one each. */
+ characters: number
+ /** Newline-delimited lines (0 for empty text, otherwise `\n` count + 1). */
+ lines: number
+}
+
+/**
+ * Derive the word / character / line counts for a draft.
+ *
+ * - `characters` counts Unicode code points (`[...text].length`) rather than
+ * UTF-16 units, so a single emoji counts as one character.
+ * - `words` splits the trimmed text on any run of whitespace; empty or
+ * whitespace-only input yields `0`.
+ * - `lines` is the number of `\n`-delimited lines; empty input yields `0`.
+ */
+export function computeComposerCounts(text: string): ComposerCounts {
+ const characters = [...text].length
+ const trimmed = text.trim()
+ const words = trimmed === '' ? 0 : trimmed.split(/\s+/u).length
+ const lines = text === '' ? 0 : text.split('\n').length
+ return { words, characters, lines }
+}
+
+/**
+ * Whether the count indicator should be shown for a given draft.
+ *
+ * Only when there is at least one non-whitespace character — an empty (or
+ * purely whitespace) composer stays clean.
+ */
+export function shouldShowComposerCount(text: string): boolean {
+ return text.trim().length > 0
+}
diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md
index 51235562a..e981514d7 100644
--- a/docs/loop/feature-ledger.md
+++ b/docs/loop/feature-ledger.md
@@ -32,9 +32,15 @@ log, not the system of record.
| slug | title | source | feasibility | status | issue | pr | branch | updated | notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
-| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). |
-| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. |
-| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. |
-| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |
+| composer-word-count | Live word/character/line count indicator in the chat composer | Codex/Claude desktop composer counters + editor status bars | frontend-only | pr-open | [#60](https://github.com/modelstudioai/openwork/issues/60) | [#61](https://github.com/modelstudioai/openwork/pull/61) | loop/composer-word-count | 2026-07-06 | Pure `computeComposerCounts` module (+unit tests) → `ComposerCountIndicator` in the composer toolbar, derived from the existing `input` string; hidden when empty / in compact mode. 3 new plural-neutral i18n keys (`chat.composer{Words,Characters,Lines}`) in all 7 locales. typecheck:all zero-delta (11 pre-existing baseline errors, none in touched files); unit tests 10/10; i18n parity OK; renderer build ✅; assertion transpiles. **CDP could not run locally**: the e2e build 403s on the Electron binary download and the `libsignal` WhatsApp-worker dep (same egress block as prior loop PRs); `composer-count.assert.ts` included for CI/reviewer. |
+| shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | Codex keypress-search + VS Code / Claude keybindings search | frontend-only | pr-open | [#58](https://github.com/modelstudioai/openwork/issues/58) | [#59](https://github.com/modelstudioai/openwork/pull/59) | loop/shortcuts-search | 2026-07-06 | Reconciled from GitHub. Filters shortcut rows by label + key token; reuses `common.*` (zero new keys). |
+| command-palette-recents | "Recently used" group in the Command Palette (⌘K) | VS Code / Raycast / Linear / Claude recents | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) | loop/command-palette-recents | 2026-07-06 | Reconciled from GitHub. localStorage-persisted recents; one new key `commands.recent`. |
+| thinking-menu-shortcut | ⌘⇧E keyboard shortcut to open the composer thinking menu | Claude Code Desktop effort-menu shortcut (⌘⇧E) | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-menu-shortcut | 2026-07-06 | Reconciled from GitHub. Bridges action registry → composer dropdown via scoped custom event. |
+| composer-prompt-history | Up/Down prompt history recall in the composer | Codex composer ↑ recall + shell history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) | loop/composer-prompt-history | 2026-07-06 | Reconciled from GitHub. localStorage `craft-prompt-history`; pure nav module + unit tests. |
+| reduce-motion | "Reduce motion" accessibility toggle in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-06 | Reconciled from GitHub. Root `MotionConfig` + CSS guard; localStorage `craft-reduce-motion`. |
+| composer-expand | Expand/collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop maximize composer | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-06 | Reconciled from GitHub. Toggles a tall minHeight/maxHeight on the editable area. |
+| scroll-to-bottom | "Jump to latest" scroll-to-bottom button in the transcript | Claude/ChatGPT/Codex desktop jump-to-latest | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-06 | Reconciled from GitHub. Floating chevron over existing sticky-bottom scroll state; adds `seed()` harness hook. |
+| app-search-cmdf-bug | Cmd+F (`app.search`) shortcut doesn't activate session search | OpenWork bug report | frontend-only | blocked | [#43](https://github.com/modelstudioai/openwork/issues/43) | — | — | 2026-07-06 | Bug, not a feature. Open, no PR/branch yet. Needs a seeded-session repro the headless harness can't produce; left for a dedicated run. |
+| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-06 | Merged into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |
| command-palette | Global command palette (⌘K/Ctrl+K) to search & run any action | Claude Code Desktop ⌘K / VS Code & Codex ⌘⇧P / Linear ⌘K | frontend-only | merged | [#41](https://github.com/modelstudioai/openwork/issues/41) | [#42](https://github.com/modelstudioai/openwork/pull/42) | loop/command-palette | 2026-07-02 | Merged into `main`. Reuses action registry `execute()` + cmdk primitives; zero new i18n keys. CDP e2e 2/2 pass. typecheck/test +0 vs main. |
| settings-search | Searchable/filterable settings navigation | Claude Code Desktop / VS Code / Codex desktop settings search | frontend-only | merged | [#39](https://github.com/modelstudioai/openwork/issues/39) | [#40](https://github.com/modelstudioai/openwork/pull/40) | loop/settings-search | 2026-07-01 | Merged into `main`. Filters `SettingsNavigator` by title+description; reuses `common.search`/`common.noResultsFound` (no new locale keys). Also hardened `e2e/app.ts` teardown (per-launch profile dir + setsid process-group kill) so multiple CDP assertions run under headless xvfb. |
diff --git a/e2e/assertions/composer-count.assert.ts b/e2e/assertions/composer-count.assert.ts
new file mode 100644
index 000000000..32d7329e0
--- /dev/null
+++ b/e2e/assertions/composer-count.assert.ts
@@ -0,0 +1,132 @@
+/**
+ * Feature assertion: the composer's live word / character count indicator.
+ *
+ * Drives the real built app over CDP entirely in the draft (no-session) state —
+ * no seeded conversation and no backend — through the full path:
+ * empty composer shows no count indicator → typing real text makes it appear
+ * with the correct word/character counts → appending updates the counts live →
+ * clearing the draft hides the indicator again.
+ *
+ * Asserting the indicator is *absent* while empty and *present with correct
+ * counts* after typing (and gone again after clearing) proves it reflects the
+ * actual draft content reactively, not merely that a static element renders.
+ */
+
+import type { Assertion } from '../runner';
+
+/** The composer's contenteditable carries this stable tutorial hook. */
+const EDITOR = '[data-tutorial="chat-input"]';
+const COUNT = '[data-testid="composer-count"]';
+
+/** Read a numeric data-* attribute off the count indicator (or null if absent). */
+const readCountAttr = (attr: string) =>
+ `(() => { const el = document.querySelector(${JSON.stringify(
+ COUNT,
+ )}); return el ? el.getAttribute(${JSON.stringify(attr)}) : null; })()`;
+
+/** Focus the composer editor and place a collapsed caret at the end of its content. */
+const FOCUS_CARET_END_EXPR = `(() => {
+ const el = document.querySelector(${JSON.stringify(EDITOR)});
+ if (!el) return false;
+ el.focus();
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ range.collapse(false);
+ const sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ return true;
+})()`;
+
+/** Select the composer's entire content and delete it (fires a real input event). */
+const CLEAR_EDITOR_EXPR = `(() => {
+ const el = document.querySelector(${JSON.stringify(EDITOR)});
+ if (!el) return false;
+ el.focus();
+ document.execCommand('selectAll');
+ document.execCommand('delete');
+ return true;
+})()`;
+
+const assertion: Assertion = {
+ name: 'composer shows a live word/character count that tracks the draft',
+ async run(app) {
+ const { session } = app;
+
+ // App fully mounted.
+ await session.waitForFunction(
+ '!document.getElementById("_loader") && (document.getElementById("root")?.childElementCount ?? 0) > 0',
+ { timeoutMs: 30000, message: 'React UI did not mount' },
+ );
+
+ // Reach the ready AppShell (not onboarding / workspace picker) — the same
+ // stable, non-localized anchor the other composer assertions wait on.
+ await session.waitForSelector('[aria-label="Craft menu"]', {
+ timeoutMs: 30000,
+ message: 'app did not reach the ready AppShell state',
+ });
+
+ // The composer's contenteditable renders.
+ await session.waitForSelector(EDITOR, {
+ timeoutMs: 20000,
+ message: 'composer editor did not render',
+ });
+
+ // 1. While the draft is empty, the count indicator is absent.
+ if (await session.evaluate(`!!document.querySelector(${JSON.stringify(COUNT)})`)) {
+ throw new Error('count indicator was present for an empty composer');
+ }
+
+ // 2. Type real text and assert the indicator appears with the right counts.
+ if (!(await session.evaluate(FOCUS_CARET_END_EXPR))) {
+ throw new Error('could not focus the composer editor');
+ }
+ await session.send('Input.insertText', { text: 'hello world' });
+
+ await session.waitForFunction(`document.querySelector(${JSON.stringify(COUNT)})`, {
+ timeoutMs: 8000,
+ message: 'count indicator did not appear after typing',
+ });
+
+ const words1 = await session.evaluate(readCountAttr('data-word-count'));
+ const chars1 = await session.evaluate(readCountAttr('data-char-count'));
+ if (words1 !== '2') {
+ throw new Error(`expected 2 words after typing "hello world", saw ${JSON.stringify(words1)}`);
+ }
+ if (chars1 !== '11') {
+ throw new Error(
+ `expected 11 characters after typing "hello world", saw ${JSON.stringify(chars1)}`,
+ );
+ }
+
+ // 3. Appending more text updates the counts live.
+ if (!(await session.evaluate(FOCUS_CARET_END_EXPR))) {
+ throw new Error('could not re-focus the composer editor before appending');
+ }
+ await session.send('Input.insertText', { text: ' again' });
+
+ await session.waitForFunction(
+ `(() => { const el = document.querySelector(${JSON.stringify(
+ COUNT,
+ )}); return el && el.getAttribute('data-word-count') === '3'; })()`,
+ { timeoutMs: 8000, message: 'word count did not update to 3 after appending text' },
+ );
+ const chars2 = await session.evaluate(readCountAttr('data-char-count'));
+ if (chars2 !== '17') {
+ throw new Error(
+ `expected 17 characters after appending " again", saw ${JSON.stringify(chars2)}`,
+ );
+ }
+
+ // 4. Clearing the draft hides the indicator again.
+ if (!(await session.evaluate(CLEAR_EDITOR_EXPR))) {
+ throw new Error('could not clear the composer editor');
+ }
+ await session.waitForFunction(
+ `!document.querySelector(${JSON.stringify(COUNT)})`,
+ { timeoutMs: 8000, message: 'count indicator did not disappear after clearing the draft' },
+ );
+ },
+};
+
+export default assertion;
diff --git a/packages/shared/src/i18n/locales/de.json b/packages/shared/src/i18n/locales/de.json
index d407e5dbb..64013715c 100644
--- a/packages/shared/src/i18n/locales/de.json
+++ b/packages/shared/src/i18n/locales/de.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "Entwurf löschen",
"chat.clickForTaskActions": "Für Aufgabenaktionen klicken",
"chat.clickToOpen": "Klicken um {{name}} zu öffnen",
+ "chat.composerCharacters": "Zeichen: {{count}}",
+ "chat.composerLines": "Zeilen: {{count}}",
+ "chat.composerWords": "Wörter: {{count}}",
"chat.connectionDefault": "Standard dieser Verbindung",
"chat.connectionUnavailable": "Verbindung nicht verfügbar",
"chat.connectionUnavailableDescription": "Die von dieser Sitzung verwendete Verbindung wurde entfernt. Erstellen Sie eine neue Sitzung, um fortzufahren.",
diff --git a/packages/shared/src/i18n/locales/en.json b/packages/shared/src/i18n/locales/en.json
index 6a06e456e..ee2a58db7 100644
--- a/packages/shared/src/i18n/locales/en.json
+++ b/packages/shared/src/i18n/locales/en.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "Clear draft",
"chat.clickForTaskActions": "Click for task actions",
"chat.clickToOpen": "Click to open {{name}}",
+ "chat.composerCharacters": "Characters: {{count}}",
+ "chat.composerLines": "Lines: {{count}}",
+ "chat.composerWords": "Words: {{count}}",
"chat.connectionDefault": "Connection default",
"chat.connectionUnavailable": "Connection Unavailable",
"chat.connectionUnavailableDescription": "The connection used by this session has been removed. Create a new session to continue.",
diff --git a/packages/shared/src/i18n/locales/es.json b/packages/shared/src/i18n/locales/es.json
index 6461320a6..e42fab4a3 100644
--- a/packages/shared/src/i18n/locales/es.json
+++ b/packages/shared/src/i18n/locales/es.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "Borrar borrador",
"chat.clickForTaskActions": "Haz clic para acciones de tarea",
"chat.clickToOpen": "Clic para abrir {{name}}",
+ "chat.composerCharacters": "Caracteres: {{count}}",
+ "chat.composerLines": "Líneas: {{count}}",
+ "chat.composerWords": "Palabras: {{count}}",
"chat.connectionDefault": "Predeterminado de esta conexión",
"chat.connectionUnavailable": "Conexión no disponible",
"chat.connectionUnavailableDescription": "La conexión usada por esta sesión se ha eliminado. Crea una nueva sesión para continuar.",
diff --git a/packages/shared/src/i18n/locales/hu.json b/packages/shared/src/i18n/locales/hu.json
index 641c6d16f..d89d62a94 100644
--- a/packages/shared/src/i18n/locales/hu.json
+++ b/packages/shared/src/i18n/locales/hu.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "Piszkozat törlése",
"chat.clickForTaskActions": "Kattints a feladatműveletekért",
"chat.clickToOpen": "Kattints a(z) {{name}} megnyitásához",
+ "chat.composerCharacters": "Karakterek: {{count}}",
+ "chat.composerLines": "Sorok: {{count}}",
+ "chat.composerWords": "Szavak: {{count}}",
"chat.connectionDefault": "A kapcsolat alapértelmezése",
"chat.connectionUnavailable": "Kapcsolat nem érhető el",
"chat.connectionUnavailableDescription": "A munkamenet által használt kapcsolatot eltávolították. A folytatáshoz hozz létre egy új munkamenetet.",
diff --git a/packages/shared/src/i18n/locales/ja.json b/packages/shared/src/i18n/locales/ja.json
index 5e2191990..5152d714f 100644
--- a/packages/shared/src/i18n/locales/ja.json
+++ b/packages/shared/src/i18n/locales/ja.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "下書きをクリア",
"chat.clickForTaskActions": "クリックしてタスクアクションを表示",
"chat.clickToOpen": "クリックして{{name}}を開く",
+ "chat.composerCharacters": "文字: {{count}}",
+ "chat.composerLines": "行: {{count}}",
+ "chat.composerWords": "単語: {{count}}",
"chat.connectionDefault": "この接続のデフォルト",
"chat.connectionUnavailable": "接続を利用できません",
"chat.connectionUnavailableDescription": "このセッションで使用していた接続は削除されました。続行するには新しいセッションを作成してください。",
diff --git a/packages/shared/src/i18n/locales/pl.json b/packages/shared/src/i18n/locales/pl.json
index f85aa9c9f..6ec1ed265 100644
--- a/packages/shared/src/i18n/locales/pl.json
+++ b/packages/shared/src/i18n/locales/pl.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "Wyczyść szkic",
"chat.clickForTaskActions": "Kliknij, aby zobaczyć akcje zadania",
"chat.clickToOpen": "Kliknij, aby otworzyć {{name}}",
+ "chat.composerCharacters": "Znaki: {{count}}",
+ "chat.composerLines": "Wiersze: {{count}}",
+ "chat.composerWords": "Słowa: {{count}}",
"chat.connectionDefault": "Domyślne dla tego połączenia",
"chat.connectionUnavailable": "Połączenie niedostępne",
"chat.connectionUnavailableDescription": "Połączenie używane przez tę sesję zostało usunięte. Utwórz nową sesję, aby kontynuować.",
diff --git a/packages/shared/src/i18n/locales/zh-Hans.json b/packages/shared/src/i18n/locales/zh-Hans.json
index 845231913..d32869bd1 100644
--- a/packages/shared/src/i18n/locales/zh-Hans.json
+++ b/packages/shared/src/i18n/locales/zh-Hans.json
@@ -127,6 +127,9 @@
"chat.clearDraft": "清除草稿",
"chat.clickForTaskActions": "点击查看任务操作",
"chat.clickToOpen": "点击打开 {{name}}",
+ "chat.composerCharacters": "字符:{{count}}",
+ "chat.composerLines": "行数:{{count}}",
+ "chat.composerWords": "词数:{{count}}",
"chat.connectionDefault": "此连接的默认值",
"chat.connectionUnavailable": "连接不可用",
"chat.connectionUnavailableDescription": "此会话使用的连接已被删除。请创建一个新会话以继续。",