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": "此会话使用的连接已被删除。请创建一个新会话以继续。",