diff --git a/AGENTS.md b/AGENTS.md index 1e257ecf..0896990a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,12 @@ macOS note: the vendored binary is unsigned — if Gatekeeper blocks it: `harmoniqs/opencode` (thin fork, patch stack in its `AMICODE-PATCHES.md`). Rebrand/UI work happens THERE, product logic lives HERE in config/plugin/scores (Layer 0). To change the fork, see "Changing opencode (the vendored fork)" below. +- **`packages/app-bundle/overlay/` is a read-only tracking copy** — never edit the overlay + directly. It is copied FROM the opencode fork, never TO it, and is not a build input. To + change any app-layer file (components, pages, styles), edit the fork + (`~/harmoniqs/opencode` or `$AMICODE_OPENCODE_SRC`), rebuild with + `pnpm --filter amicode opencode:build`, then `sync:apply`. Edits to the overlay + silently vanish on the next sync. - `packages/extension/opencode-plugin/` executes inside opencode's Bun runtime — it is NOT part of the extension bundle; keep it dependency-free; exactly one export. - `packages/extension/scores/` — interview flows as data. New user path = new `SCORE.md` @@ -123,6 +129,20 @@ built with `OPENCODE_CHANNEL=latest` (→ `"prod"`) compiles the features in but `opencode:build` and the release workflow both force `dev`; `scripts/assert_ui_gate.sh` fails CI and release if a binary ever ships with the gate off. +**Overlay sync:** `packages/app-bundle/overlay/` is a **tracking copy** of the fork's diff +against upstream — it is NOT a build input (the binary builds from the fork directly). The fork +is the source of truth for all app source files. **Never edit the overlay directly; never copy +overlay → fork** for files that exist in both; that overwrites newer fork code with stale +overlay snapshots. + +After editing the fork: +```bash +pnpm --filter @amicode/app-bundle sync:check # detect overlay ↔ fork drift +pnpm --filter @amicode/app-bundle sync:apply # fix: copy fork → overlay + update hashes +``` +`opencode:build` warns on drift but does not block. The `sync:apply` command is one-way +(fork → overlay) and updates `manifest.json` hashes automatically. + ## Releasing & publishing (amicode → Marketplace) Two channels. **`vX.Y.Z-alpha.N`** is internal — the `alpha.N` mirrors the vendored fork's diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 90ab0374..e27e22fe 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -919,7 +919,10 @@ "packages/sdk/js/src/v2/gen/sdk.gen.ts": "M", "packages/sdk/js/src/v2/gen/types.gen.ts": "M", "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "M", - "packages/session-ui/src/v2/components/prompt-input/index.tsx": "M" + "packages/session-ui/src/v2/components/prompt-input/index.tsx": "M", + "packages/session-ui/src/v2/components/diff-gutter-extension.ts": "A", + "packages/session-ui/src/v2/components/editable-diff-view-core.ts": "A", + "packages/session-ui/src/v2/components/editable-diff-view.tsx": "A" }, "files": { "packages/app/CONTEXT.md": "7dc4bf176a32ee578a54593a58a6b97907ffc5a9baf7ae7c16a8fa920d8e6e86", @@ -928,11 +931,11 @@ "packages/core/schema.json": "79ef4e2f0dd8103ac7a4b6ff3ddb0fd10764655f1dc678c1c401dfc43be9a1aa", "packages/session-ui/bunfig.toml": "82585987c8258caf58d07c6aaf4e49b7338e4da16f97c785af398aace9e6e722", "packages/session-ui/happydom.ts": "d81d6b14e988f058fa502c841df34b55307c57fc94a26fb4f8a5deb6aa0d4834", - "packages/session-ui/package.json": "87c1d5b1070f94338747912985a2ce03542de9857534b8411365223436f44a90", + "packages/session-ui/package.json": "9391c23dd35b0ce47d0952ffb25a6d1112055ac323cb7ddb22e43990f4e5928b", "packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629", "packages/app/public/amico.svg": "a14b9d543d895bcdf0758f7b9ef5908ee0acaac794446494af059b159247db8f", "packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6", - "packages/app/src/app.tsx": "6c6cb948e0486aef526e3326f214de0ef55106f6bf77154a5dd6b4212a296265", + "packages/app/src/app.tsx": "26db81a03c2f303701037cd2d3f0bf91b585193f01b6b31db5f869f4f8e76fc7", "packages/app/src/design-polish.css": "42cc6efaefe9a71dedd12fcb0bf2549453d9a46097cb025cf087064b29d3ccce", "packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c", "packages/app/src/index.css": "08179e06ce2d419a2d98acc96025f91c7709062ea9f3ad245e88dc35e75ff9f7", @@ -995,8 +998,8 @@ "packages/app/src/components/pane-bridge.tsx": "756b3306b51c963fac6778716f6c6c4626f0f3bf75c93b7cb4cab58ea0bf3d4e", "packages/app/src/components/profile-popover.tsx": "eff742e90544455f13234a9351bb1b82189663cba2cd3172bdcea1a8a37284e8", "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "946b718c2164da394602f26e4d9919d752da0d0b9bee714f345ba16c274542da", - "packages/app/src/components/prompt-input-v2.tsx": "693c23781b35fac34f8a885ce117ecab947883b6a8621657a8e0545a9ab20050", - "packages/app/src/components/prompt-input.tsx": "b30d8953b79464a917c32aa03feb2f78c6b4a6195df770f1ee931b535105df40", + "packages/app/src/components/prompt-input-v2.tsx": "9ec794b2b9dbcfbfeb9d880df0038087fe6b8df219c5c96b1971f7fc766b67d2", + "packages/app/src/components/prompt-input.tsx": "1bc71d585a5ada2caf6d190d702dcabbfb56642e740a08ad6aedf078809b60e5", "packages/app/src/components/prompt-project-selector.tsx": "0157c954b938bce2b5aca9a82f4a72338e361f4c9027e55a7fd5a14a1654d294", "packages/app/src/components/report-bug-button.css": "909809b6241d53c386aa4c8ea34539ba162a8998fff755834c783025bc53a5fd", "packages/app/src/components/report-bug-button.stories.tsx": "82aed22bd3fcca448841205dee759ca0f0de92562a99d07c696eee5bd29e807b", @@ -1027,14 +1030,14 @@ "packages/app/src/components/vault-browser-model.ts": "d9ef6ff588a96ae79b13c3ae0c2a442a42b59021dd501b3a17bd810dda6f7b20", "packages/app/src/components/vault-browser.tsx": "509efa874d69bcd07ef2495a025bc8de2b2fa582736003ff709b4ca76096274b", "packages/app/src/components/vault-panel.tsx": "1d9f06f42b94ffb94a28bfd6d2693a36c75ceb969bcd434081b4cd2fbf99665c", - "packages/app/src/components/windows-app-menu.tsx": "220b9859214c267deb2957c6a4175083e4a66c8316ba28752f186e0931d38ab5", + "packages/app/src/components/windows-app-menu.tsx": "460669a129e9be61ab2d5f51fe6ecad73579a395295d5f724f235b988cdc667e", "packages/app/src/components/workbench-panel.tsx": "d16f477044098842a214a2e16f9be719e7688a04bcb575889ca544988f23c690", "packages/app/src/context/command.tsx": "06ffdc83f39186481bba8120e4bc1b14c2160d1dc789a29e0a07ffc7c5a2cd35", "packages/app/src/context/directory-sync.ts": "0d9e6aa3cff6c6654c98928f59c4e2fcced04f42b167184eea77be04c00371ee", - "packages/app/src/context/language.tsx": "0f7b1cbd33071f293c15b66e3a900dfd07347f9eb93d8cf61df0fac3bc4effc5", + "packages/app/src/context/language.tsx": "cb545e04c128bf981b2b72ac407f9220cf9e80337fe8c87482ffe85407e0cf9a", "packages/app/src/context/layout-tabs.test.ts": "4d9fdbe963306f164b2f48eac3b6a28c5a703c1758bf14b2cc4796720d8fa72c", "packages/app/src/context/layout-tabs.ts": "741506ce165f68cdb0b8f2931a2dad3e26c05880f9286c3daf04ec979bac21c5", - "packages/app/src/context/layout.tsx": "54dbe82c704c92109eb392b512fe2d828a5ff00a14315173ad8923bf0621d788", + "packages/app/src/context/layout.tsx": "29a39367295b0a70fa0f7603a6b91b4d64ad0a134616ebfe79488245caaad293", "packages/app/src/context/local-agent.test.ts": "a5a9d60bb4401d409218cc247c1cc06ede8b7878ddf01dc6080328f54eb9cade", "packages/app/src/context/local-agent.ts": "0aab67e695dc3bb45a733ac0df80a0a5e14cfe29b2375b6dc3cd07fbccea33e2", "packages/app/src/context/local.tsx": "3ab8b9fc2db082df4ba485373679f00a95d0ffe3c691a0afee1dc53db7eabd9e", @@ -1050,26 +1053,26 @@ "packages/app/src/context/vault-panel.ts": "f91cb5fb49a9f0d46bd2a1b131f9a218b30147e2ad3617d2a2dc4f1e14599a53", "packages/app/src/context/workbench.tsx": "69a44da2b69989bd2c67ebffdb49941841f3f4718d6a70d659c828b57928d9eb", "packages/app/src/context/zoom-keybind.test.ts": "a1385c93639687a7dac7d21f8f02841d82a55e42465ce3836b97b16c82b52469", - "packages/app/src/i18n/ar.ts": "feca57fdbf07f5fd797a249b3f4d318adcee320bdfbf4c2f6dcaf08f430a1916", - "packages/app/src/i18n/br.ts": "339afc9f492288cef5a340aa03e583319ecc5aaf5e4a6f5ea26f94fbb961535a", - "packages/app/src/i18n/bs.ts": "d60e8b4704a8422d55df67ff2da47f331646a2edb57da5f416043cb38f09004a", - "packages/app/src/i18n/da.ts": "a50390b9d8c38c4250c17c1623f018ba12293505f39a31ec941fa5bb6fd83769", - "packages/app/src/i18n/de.ts": "80b8f4ff421c227b5259fed870cce12317f7bec627c1e1c38f525d9122158424", + "packages/app/src/i18n/ar.ts": "28cd812279cf0435d59b90912cb94044d95b9476b9f125de219e856928230681", + "packages/app/src/i18n/br.ts": "4dbccb7c1b618de7adb8182a26fd8033b42937613d9a8c31dbb329e0dce5e64f", + "packages/app/src/i18n/bs.ts": "09f6e5fed81e06a1dcabe2cf011b4cdbbc04ce7dce3b62792cca33867eeeb009", + "packages/app/src/i18n/da.ts": "3b9aa81a934df108e3201a771452a0df1d1e3410516ef8b5e78d1fdc89e05b8c", + "packages/app/src/i18n/de.ts": "3e7c56e1b38c6c8555fe9c944097b87de563c43990ac62d0d341b820fa292724", "packages/app/src/i18n/desktop-native.ts": "8fed66d36a0b6cdbf50e3a50f14b6f8ab3570e342e0905e5a926d7dc6c57f8fa", - "packages/app/src/i18n/en.ts": "69e3cd58cc0f6ecb322ef97e85e1a034e6a5706057c90bab67f6d4c41666c2ea", - "packages/app/src/i18n/es.ts": "aa522d3aaef056f8eed14ae41c9a0af207a271b2dfd32bde3ec0b6e337adf904", - "packages/app/src/i18n/fr.ts": "43400c7c7c3fb90fd53e05cd6a716816ffc44781c5adf534a3418e2595230efb", - "packages/app/src/i18n/ja.ts": "ee56913545adec0552aac43195a8a515677c8b9549f1b23b2075946cb14b68a6", - "packages/app/src/i18n/ko.ts": "c9f8ff5f7ef94c9b7930c7daf405a6ff98fb864bb3ee3d344c0baa6196ca860e", - "packages/app/src/i18n/no.ts": "680edde7a34098a3090b33662a8a90bab79d6dee5bcceda0c2baf8614201f404", - "packages/app/src/i18n/parity.test.ts": "3a05e1bc47321d4a00ae2ae898104e7de5398170e74f888d2f8a0f6e259866e6", - "packages/app/src/i18n/pl.ts": "807f2413d30946dcb23aa107c4f56d38a62e21ae5bf2ac8b28de89e4031a9fa9", - "packages/app/src/i18n/ru.ts": "4a25cf8324a9cfd870c7150c0b6d9276b511cddf4a0d568f9aecc7b014e55217", - "packages/app/src/i18n/th.ts": "5f507ba4f6487daf3ed7ca7c2efec7e2c249070bce72c58897b239f14b4012a8", - "packages/app/src/i18n/tr.ts": "b606bcbe537df88b5c3946bd48031ce29a36af5422b5ff7354aa0b1cfdc408ec", - "packages/app/src/i18n/uk.ts": "3e4ee340c269f71d7335199bbe756280586fc32e9203928c53136717af000e2f", - "packages/app/src/i18n/zh.ts": "9ac5ca7e0ec735e0a2919f7206b9ce35f8d69c428b00d5edc6e7bc52eac373ff", - "packages/app/src/i18n/zht.ts": "4e8d9682bee8e8770ad91214fabc286014d52fa2b230cef42935e9829ff350b2", + "packages/app/src/i18n/en.ts": "60a5ef27885a1dbfd52407cdded8c2bb82ed855099eacbe22b6c50c232a420ce", + "packages/app/src/i18n/es.ts": "4da12deea30d5a59a1951b117b796ae200f9e276a253bbf12f38ce493bcabf61", + "packages/app/src/i18n/fr.ts": "b4e44f910b6329384d6b829f5062a73b7ae63c8d9095b04aaf80c1335603674d", + "packages/app/src/i18n/ja.ts": "659865ed067c1dde1444c8561de26eac95e54dcb2407267f14b9f45994c5ac4f", + "packages/app/src/i18n/ko.ts": "df31dc838120bcc0b31d7837ad87bde0c573b519e37080e6bce5cbf9dcfe08cf", + "packages/app/src/i18n/no.ts": "25e8ef730b71829e71c232a58797b7130f17461fb6b707bc9ec04c22ac4e389c", + "packages/app/src/i18n/parity.test.ts": "2c8a1d3af6648cbce73c4fee45008c9928d1126a6b044cb0a32dbc566967c161", + "packages/app/src/i18n/pl.ts": "26ac13d31f7b0c835e4b5c3c6a5d118b610098487ec3132ab6e2cdf2f28ce02a", + "packages/app/src/i18n/ru.ts": "22c7053a3323ac70607e4c298ed8e39cd93188712a3b521760ffd18c20e3b206", + "packages/app/src/i18n/th.ts": "4fb6bc8ad159c1dc6968e04ecb7d9b13502518ccc011e0b8cf31457f9696e7f4", + "packages/app/src/i18n/tr.ts": "a1323a199ed765e3a85e2f231213d4f0732c7eedbc57fb497d34fca0775b2a11", + "packages/app/src/i18n/uk.ts": "1e19646ad7c00359633c31afccd26ad2e818b4daeb13dbeefe1f6645f110c157", + "packages/app/src/i18n/zh.ts": "f029176c561e5ae2c05311a0f03f77308ac97982c90b6ddca2a350f5f34079a5", + "packages/app/src/i18n/zht.ts": "0eca67807223cafee656cf1b06e92721bf9d51608c03e1dc0a1c9204fc675a97", "packages/app/src/pages/error.tsx": "db9ba0847cfb205100ad6133cce30c6ea2ce983802af44947e6d2b746c2a6ec5", "packages/app/src/pages/home-projects.test.ts": "a90155c37210a0e5b7a095bbfc3ba2d40681f02927d9726148a706bed5ba7891", "packages/app/src/pages/home-projects.ts": "d55124578dc839b98e2084b488d818e30e4a1abf1ae264499b83e403c2aabca7", @@ -1081,7 +1084,7 @@ "packages/app/src/pages/new-session-landing.test.ts": "c0f47e4096e6a20fa59dac2b10f7285d3ef45e9289bf6f0ad75c1d9e4dd1117f", "packages/app/src/pages/new-session-landing.ts": "528d27cd5d441b521674328fb7812fb2acf77233e9e436d081c98ce611e8b268", "packages/app/src/pages/new-session.tsx": "5a9c699ab5579ab4ff14e67fa8696bf4d941c6e4ea1f9150fec6ca52c9435826", - "packages/app/src/pages/session.tsx": "adc8a5313867a68f4e2e954143fe1098fe1b66274442e5a5c30e85bbfcaacc77", + "packages/app/src/pages/session.tsx": "cf3b57f7efa85080d9a360ad848aeab762603c092a50f4c3f7a58a2988fc1161", "packages/app/src/utils/amicode-bridge.ts": "5104a04c7822f80dc0c564619e7dd2d850ba50035dbc86ea875604ecd97df05f", "packages/app/src/utils/amicode-bug-report.test.ts": "b1bbf49230ff3aa6d1a43ffa9165ba5a84ca4ee18a6d49f52ee5cd887c3e6444", "packages/app/src/utils/amicode-bug-report.ts": "d55d9cecfe17719c0d4a0ca498345c7eff6fca1de6b645e4bc3fecb40f2f4645", @@ -1159,7 +1162,7 @@ "packages/session-ui/src/components/message-part-user.ts": "1f2c8cb4efff5274863a133f369a7397cac804678e953740f356ae03b71d475c", "packages/session-ui/src/components/message-part.css": "ae592deb33739738f6a7fc6a3460b7146803bdd05cbbf46f1cfd2c5cec421392", "packages/session-ui/src/components/message-part.test.ts": "7c66430837fa3d2607657e57e35720ae68dc5e84662e12826713a938301eeb7b", - "packages/session-ui/src/components/message-part.tsx": "f15243b0d463902f026275f9fa7236663e06eb3faba779503429f1083c46fa4e", + "packages/session-ui/src/components/message-part.tsx": "e94f064b10030622250c8e9bd0c869796cc6dda20fbcb643736c8b4802b1b2b7", "packages/session-ui/src/components/session-retry.tsx": "2cb1513d73275a269894f601e8c1d29bc790b14927134fe36177f2b371888721", "packages/session-ui/src/components/session-review.css": "ec8c5b359dd4b7ff80d7fad8906359678adade9ce62c9d74aedc000b646db341", "packages/session-ui/src/components/session-turn.css": "792915f3373134eadf3cd51e1d4bfa88ec347e6b037b6caf60b639ec5e74fbd8", @@ -1337,26 +1340,26 @@ "packages/ui/src/context/dialog.tsx": "876f8a5dc26ebb98f1f5e9e528e7d0bd7aa79c0a107b62027dd9fe78ed2cc620", "packages/ui/src/context/marked-code-span.test.ts": "8ceabe7c81b1cf1291e198748ea26680c3b60c3a38546378070a47b9b14e9708", "packages/ui/src/context/marked-code-span.ts": "0d6444f6996953b64251abbae67ce6e291dd3b39f5d7cf51d8257d4eafde31b4", - "packages/ui/src/context/marked-math.test.ts": "f061c6b93ece059e5d19af921b7a70a962034323d7679899638ad45a7f794cc5", + "packages/ui/src/context/marked-math.test.ts": "0fe460c09956335a469b382ff631809381875465cbcaf7e7c599f86240b0227c", "packages/ui/src/context/marked-parser.tsx": "cb72ef9e104111c316aa5b80fab01ba8688571cbce9ccadd9a9fb7ec0d98bbfd", - "packages/ui/src/i18n/ar.ts": "0bd76ff7f1e23d2f4939c1b47c4e55ed75d672c177d7b869a077b0f0776007be", - "packages/ui/src/i18n/br.ts": "028355c4c5b33863ea2d21b9baf0f965db8df577c70d73ad0e3d3f9022643273", - "packages/ui/src/i18n/bs.ts": "de06bac009216acb511a33eaf43baaca6734582f1b4d134f59c1790d4ad9d433", - "packages/ui/src/i18n/da.ts": "81c6fe50341f314c4026bbf1910fb0573760a8bada417fad5e1ed7136cc36fac", - "packages/ui/src/i18n/de.ts": "a884e423a239a13cae07d3eeb001ec48d991d68fc9c014c721c9afd718fcf61e", - "packages/ui/src/i18n/en.ts": "89ade5afe6f4ad985756fb2425b0084b20554a8618b97c3963f8b619a52de610", - "packages/ui/src/i18n/es.ts": "e15c841402ba7d543c3ec7d3ce31d45d50ab7764a370a8453a19287b0283b9e3", - "packages/ui/src/i18n/fr.ts": "f5908728f20aae242d71a4ece2d734855ec70a2a56493207c40ba52759969e50", - "packages/ui/src/i18n/ja.ts": "ad80e39b38e7a6eb0d8b2a1c9a51bf2dd28363c3628b0cd2ddcd7fb7ae71369f", - "packages/ui/src/i18n/ko.ts": "a3800bd48a1616c5e7776d3a19d193a25cb80787ab6081a9f1e6407fa604c77b", - "packages/ui/src/i18n/no.ts": "35a6fb4040dfd4cea484696406720d086e6caf71954109ba5711d76dd8f4c0f1", - "packages/ui/src/i18n/pl.ts": "7d7f2406e75f4ff066cf4e559091ae95f7583b30bd48a02bfd1ff7723ef6fece", - "packages/ui/src/i18n/ru.ts": "7b14f7b50629ff55692ee2050b0278ccb1cdc98ea539239b78898a6fa7586dab", - "packages/ui/src/i18n/th.ts": "222827ae6e7d2cda6bd5bf5592cc51552e086a0473229bdebf764d173fd8dd11", - "packages/ui/src/i18n/tr.ts": "287c526b2827b55968a0e006dd3ea69b1f382f572e6e76d46826eca4ca44812d", - "packages/ui/src/i18n/uk.ts": "72a2d962841b89a334f371d53afb18665062c4d0e09a909e9ed521c3eca582ab", - "packages/ui/src/i18n/zh.ts": "0ca5980f95f392ecf52be711707c1a508f88e27f169fc7a07c64df3ed5de2e4d", - "packages/ui/src/i18n/zht.ts": "db33b137708929117ae0fec8197109698897739ad079bc8774a5a4304216b067", + "packages/ui/src/i18n/ar.ts": "8113d320fe4a5300f1bd70150ae1695d68cb783e1c6e2be43b8a429b7dbdb711", + "packages/ui/src/i18n/br.ts": "cbde2da43ef820fe2664cb96cb973fd722156a6c8a3c5e16d18638bf014e5196", + "packages/ui/src/i18n/bs.ts": "23ea0ae34a4edaa4599b13f63f481be861e28da3b0d898987e291c7e91a9f11e", + "packages/ui/src/i18n/da.ts": "e8083f8a7ab01f73990a1eabcba817afdc663858d524e56e652ea69238cefc47", + "packages/ui/src/i18n/de.ts": "eb854fdd43f16fc2346ec43d7d1629d2714709991b3e5f7ac5ee99011324aa43", + "packages/ui/src/i18n/en.ts": "1b412a1cd0ea8f7d0ce6069d3bd94dc40778cc38c23c60191cd90071d4187ac8", + "packages/ui/src/i18n/es.ts": "a34d132adc2ad68ae3da98b6afd5070be48a0cd6cb15c4866cc7ea99deefc4ab", + "packages/ui/src/i18n/fr.ts": "4a8296dec41894e6ecb209e44564ac79c04cae73035b5447eb233cd984ef4bae", + "packages/ui/src/i18n/ja.ts": "6c13f118491c8badfac33baf449bee55779f6058792b6a54973bfe975e4c25c3", + "packages/ui/src/i18n/ko.ts": "b97f70d80c9ab7561c05ab91d461e9323c4c846d6a27bed0a41ad5fbcdf0c1db", + "packages/ui/src/i18n/no.ts": "120e5d11f5aebde00822def6fda9ae12db47cd020bdd04114cdb098d5ba67449", + "packages/ui/src/i18n/pl.ts": "8133cedc013364bf84a3902399b431f4029d6c1d57acf23f12a739127d4583e9", + "packages/ui/src/i18n/ru.ts": "11d79d60f2f678597ee2b33b51fa666f38f1e78b7ed051a686229ae0dd8aac72", + "packages/ui/src/i18n/th.ts": "afa5a46ff445461db8f069338aae760cb9cac09b370c47ced5542549c92c9235", + "packages/ui/src/i18n/tr.ts": "fac27e4bec327ff83976d9cdd8f54c2a241cb26b44c644a93c591e99f09290ed", + "packages/ui/src/i18n/uk.ts": "3531fcf92efaec359381fcc2f0cdbb644d52936a20037067451b057ef327ea1c", + "packages/ui/src/i18n/zh.ts": "6aac9e8c8973fb538effee075740035ce58a631615d39c0c8fb49375d45a56e5", + "packages/ui/src/i18n/zht.ts": "d50b7119139f53180eac64a6317341f54efc87ff76d56961a3e86ef74e3ad29b", "packages/ui/src/styles/index.css": "2eae4e3d82e992c3d99f4a91ff25b3d6e9e514b38e9bebd232e6c41009cdc1fd", "packages/ui/src/styles/theme.css": "0dc88bf675df5cdf08cfe0cc53c11eaf030a1bc0cf8b36cbeb3977367c2b362b", "packages/ui/src/theme/context.tsx": "93b0b3f8a258bc8a33d4b6a4cda7016e91f6d999bd32c60a43aaaf45eadd1227", @@ -1370,7 +1373,7 @@ "packages/app/src/components/prompt-input/clipboard-bridge.test.ts": "15f76dca1a25d024128718e4099220b79ca029263410aab55502cf9caf9b2914", "packages/app/src/components/prompt-input/clipboard-bridge.ts": "4b5a1edf59d4c85268d6ff72aa9f9d66f82d4fdb887121fb978c09caf4c5be65", "packages/app/src/components/prompt-input/context-items.tsx": "027b02e394ccd14849052bf3fcacdae1e090c71d164fa8955addfe3f7abf1758", - "packages/app/src/components/prompt-input/image-attachments.tsx": "aa451ed6aa61a68ba0e8dacd8c45137f84c62566797a5242b75a89487ef0e9bf", + "packages/app/src/components/prompt-input/image-attachments.tsx": "30cdea4285884b4c257670b57ade4266f5861e8a303d3355a591f302ed4bf965", "packages/app/src/components/prompt-input/slash-popover.tsx": "a1800173edebcd419802796470f7270acaf0ef689a72f1b81a1cedfd8968337d", "packages/app/src/components/session/context-warning-banner.tsx": "f5fab4d7a9536238b70d64488fc30d44f6a4948fdcffd910a9666f4b6c296019", "packages/app/src/components/session/index.ts": "21473290d4a1a3d0670fd878372ddf21ff22d4a0b30d1aef6c81ea906c1488b4", @@ -1399,12 +1402,12 @@ "packages/app/src/context/global-sync/event-reducer.test.ts": "600cd7654854b910f25c61b20d8ae69f043f52ed544e6bf9be5ee9e5bbd6c96e", "packages/app/src/context/global-sync/event-reducer.ts": "667fbae1259f6deed77573edd2b742beee4f825b4306b6d861670443e381a55f", "packages/app/src/context/global-sync/session-cache.test.ts": "c66d790c6336794b2b1d707e2739fe0992ccad7ed3b96187b124b5c783ef332a", - "packages/app/src/context/global-sync/session-cache.ts": "438a27e075dc0e80db822d178a5b7d7777ab5e25ee865e5652b9904fda7542de", + "packages/app/src/context/global-sync/session-cache.ts": "1849408caba0f114809f37153e7d531bf2776572433880a96c5ebd31d7b30aa3", "packages/app/src/context/global-sync/types.ts": "901c9bd1fda63dab676d33dc1a8582a29f8ee00aa20458e843aed5f93414fa84", "packages/app/src/pages/home/home-projects-view.tsx": "074f1cdd7926db9574164824666cd63f1f5bd0f8e8dfb65453f6fe8f2dca0ec8", "packages/app/src/pages/home/home-sessions-view.tsx": "5b73b18e4349a628a15e09a3810ecf2319bde222e18fe8de5d08373ac91fb37c", "packages/app/src/pages/layout/helpers.test.ts": "f1ba4e1513dbcdeacab05cf98fa73dc561a18e9d3faa975afe1dda926fb8bba6", - "packages/app/src/pages/layout/helpers.ts": "45d9ab4dcfe2c6b0ecafcaa69a4fa758556d80ec877faa6e78b271d165051d89", + "packages/app/src/pages/layout/helpers.ts": "fad59b18da9bf2cbde720627865e1b02d60168aba9f81ea7c07e9402b2fc8717", "packages/app/src/pages/layout/project-avatar-state.ts": "7aadb235d27dc45799411c724f0fd1282af82d091cbcefb0344deb7c5d0659d4", "packages/app/src/pages/layout/session-tab-avatar.tsx": "89e83e239d513f91b31f16473be531eaf82768cefe637ea5f10d752cea4ef887", "packages/app/src/pages/layout/session-tab-status.test.ts": "d6e62365948450c0acda628bc5d03233e5e6ead69649c2d2fb7e20c906007e74", @@ -1431,10 +1434,10 @@ "packages/session-ui/src/v2/components/attachment-card-v2.css": "3b4b9c23d69e76b35488bc3a65fb51211038c3e8aac015df3f531b4e97e96359", "packages/session-ui/src/v2/components/basic-tool-v2.css": "55d3aa179704deddda7149342a951229ff5561127f786dd551e5df9aea8a7835", "packages/session-ui/src/v2/components/basic-tool-v2.tsx": "ece46d761f7d51a7a0925b3df00a5c289d891e9a514ee721016eb723d87fb406", - "packages/session-ui/src/v2/components/file-name-picker.test.tsx": "9fb252f376227adb076c188baaafb4d8b563b7fec20eb5944679f665b510dbee", - "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "b83a8484f88ad4f1b6dc876c3b43fed8f2d9306c535c7723c7278c9ac2eb4683", - "packages/session-ui/src/v2/components/session-review-v2.css": "9cc0a7418e5923ac3915298410af5c02e9c170bf3b415fe56b9037f3cabb8c7e", - "packages/session-ui/src/v2/components/session-review-v2.tsx": "a683eb509c3ee927a7ed04ceffef3fd65129f812ac55f5b04c7e628bf0611831", + "packages/session-ui/src/v2/components/file-name-picker.test.tsx": "1ce9741e315b549165d390c74b5885a43580aa70e24de987f9ba8388cd521b41", + "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "456d122f4ef68d8c265ad51d95ff77bf3f8c16dd5e7e9cccaaf73a0de781778c", + "packages/session-ui/src/v2/components/session-review-v2.css": "34cfe1428aafa2c05f5025a180d56ccc5b04746ccdffc7d537d1d69068f02662", + "packages/session-ui/src/v2/components/session-review-v2.tsx": "4151286efb6e0e148246ee3eebae24a2e5d4eac2603fd5ea8546d0839a694161", "packages/session-ui/src/v2/components/tool-error-card-v2.css": "9eb0bf8b583a18159f3855465118ff6a853d0193ea783037d5c90a868fdb3ff1", "packages/ui/src/amicode/fixtures/formulation-migration.json": "e573355a5eeb29f1611e52f0fb716697281b16f899331fa7fb5ada9eac4e74bc", "packages/ui/src/assets/favicon/amico.svg": "7c64b145bdd063bacd53318336a423f0654ec702de2b4e0c4a4a384694052804", @@ -1444,7 +1447,7 @@ "packages/ui/src/v2/components/badge-v2.css": "32135a9df5bad856a10138e00a2cb3ede7a6fc0cc2398797ad6bed9335d1f845", "packages/ui/src/v2/components/dialog-v2.css": "ae6f7b1de755929ba529b9dbb0e837c80f620f00a77188b9d2c59f10b0f28bbd", "packages/ui/src/v2/components/icon-button-v2.tsx": "966e535e3efe503f054045be4476fc20d2fad36092410b52f75e4cf8de6fce6f", - "packages/ui/src/v2/components/icon.tsx": "243cbd4f869bac4d67cd2ef3c9b60e926eb79ba8e01afffd7b3ffdcc3f9e0ad1", + "packages/ui/src/v2/components/icon.tsx": "9f702e9c307524f79dfa9cad70f9bea32bda85672f917ffdb0871bb14cf13437", "packages/ui/src/v2/components/menu-v2.css": "2f262a6c456a8e3cb9027f0b8b3bb3c4327794bd80cadd3cf8c16d08e6125d4d", "packages/ui/src/v2/components/menu-v2.stories.tsx": "4ed1498d7e4cd51ce9c7459061f3540efdfae71751ff618901c78ee413951559", "packages/ui/src/v2/components/radio-v2.css": "bf6eb3457a7fc13e48356f5a7dcf3cbf462382580250a3144a745402ff88e2e6", @@ -1489,15 +1492,18 @@ "packages/app/src/pages/session/timeline/thought-rail.tsx": "d42be90ccb75c2efef3a3a2718763165a10336d08f8d48f8b33ba7dbb696128f", "packages/app/src/pages/session/timeline/timeline-row.ts": "91576b8ba89a2882881bf4e8a0019431cb118a487e1160810e0c51bf23cbccf7", "packages/app/src/pages/session/timeline/travelling-dot.test.ts": "c32a66e0d188d84d6b9693f35f29efb3ff1b2ecf30077f8250782fbf563d19b8", - "packages/app/src/pages/session/v2/accumulate-diffs.test.ts": "da0c3f950c0870257d5866793c4f2e42ea4416c2a318bc755c9845fca1a77cd7", - "packages/app/src/pages/session/v2/accumulate-diffs.ts": "f10a1e097a86f746f3fc2b861454f3b8f509e6895c54b7e3bd0317dbf3d14acf", + "packages/app/src/pages/session/v2/accumulate-diffs.test.ts": "4fed2fd6ac9496be843e6ca328a39b3cefd94cbbc8bbf36815c6da4b47fe44b6", + "packages/app/src/pages/session/v2/accumulate-diffs.ts": "1529455bb8d33ca90224505d1fb4327701a1468b7baa365e512a88a2b00b5067", "packages/app/src/pages/session/v2/review-panel-v2-state.test.ts": "ac0cf1939c164948d9a0c54cc15076b30413612d872237e0fbd0912804c1ced0", "packages/app/src/pages/session/v2/review-panel-v2-state.ts": "e27c3aa236d2862e3cbd87128c4e03e6d810974faaaadb12a7e04cdd3b531e1c", - "packages/app/src/pages/session/v2/review-panel-v2.tsx": "b573dd2f3cf227d1cf85e715668f78d71b58f237d65750016028f17aaf716727", + "packages/app/src/pages/session/v2/review-panel-v2.tsx": "67e9a692c6cb9b95b514e3122f7cf9420129ebabb4c8a758d6d2b1cb05275188", "packages/sdk/js/src/v2/gen/sdk.gen.ts": "57597d710845cc0004ba778522c9bdf088fe9f7c23d7d9a3d947038a1fdc2674", "packages/sdk/js/src/v2/gen/types.gen.ts": "40460004bb971a4f8dcc5dcd9f9643cc839f5d75b6820a103ecf7cc21235d9b6", "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "ff66078f2edc9a67a4df0d7dfd76a49be5eab27ac332b303f3412721ef7268ea", - "packages/session-ui/src/v2/components/prompt-input/index.tsx": "7f29e44b1d7fc3a4ba231d0e918e9e3fe82390602b6fb17ba7489b21a93db3d8" + "packages/session-ui/src/v2/components/prompt-input/index.tsx": "6cbd3a1e3a4fd789284526e5981c6fd2d4952f3926c5a1be0609bb6832453997", + "packages/session-ui/src/v2/components/diff-gutter-extension.ts": "aed07587c0c9a5f110f9ea91aa42679d6f9f66b2ec4de9043d6e2233a375acb8", + "packages/session-ui/src/v2/components/editable-diff-view-core.ts": "75174c6b3378639a4dfc6fedd8b0119b048fd08665111f0f9ef25247b3a95366", + "packages/session-ui/src/v2/components/editable-diff-view.tsx": "e95276e111b5a3eb0172a2066ed3a436bb917baa1b65575626f94a17923194f1" }, "upstream_base_archive_sha256": "8fd2a4e179a6a001e68f1f0986e6687be524a9a28239b8997c6b7e9a72033231" } diff --git a/packages/app-bundle/overlay/packages/app/src/app.tsx b/packages/app-bundle/overlay/packages/app/src/app.tsx index e5f88051..fa39019c 100644 --- a/packages/app-bundle/overlay/packages/app/src/app.tsx +++ b/packages/app-bundle/overlay/packages/app/src/app.tsx @@ -247,13 +247,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { function UiI18nBridge(props: ParentProps) { const language = useLanguage() - return ( - - {props.children} - - ) + return {props.children} } function LayoutCompatibility(props: ParentProps) { @@ -506,12 +500,7 @@ function AmicodeNavigateBridge() { return null } -export function AppBaseProviders( - props: ParentProps<{ - locale?: Locale - onNativeTranslations?: Parameters[0]["onNativeTranslations"] - }>, -) { +export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { return ( @@ -522,7 +511,7 @@ export function AppBaseProviders( }} > - + { diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx index c3926a8c..be6ae492 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx @@ -145,10 +145,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): t: (key, params) => language.t(key as Parameters[0], params as never), }), ) - const designPlaceholder = () => - promptDesignPlaceholder(mode(), placeholder(), (key, params) => - language.t(key as Parameters[0], params as never), - ) + const designPlaceholder = () => promptDesignPlaceholder(mode(), placeholder()) const historyComments = () => { const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx index 7eb71290..03b5b5b9 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx @@ -1491,7 +1491,6 @@ export const PromptInput: Component = (props) => { } onRemove={removeAttachment} removeLabel={language.t("prompt.attachment.remove")} - fileLabel={language.t("ui.common.file")} newLayoutDesigns={false} />
void onRemove: (id: string) => void removeLabel: string - fileLabel: string newLayoutDesigns: boolean comments?: PromptCommentItem[] commentActive?: (item: PromptCommentItem) => boolean @@ -95,7 +94,7 @@ export const PromptImageAttachments: Component = (p } > - {typeLabel(attachment.filename, attachment.mime, props.fileLabel)} + {typeLabel(attachment.filename, attachment.mime)} } diff --git a/packages/app-bundle/overlay/packages/app/src/components/windows-app-menu.tsx b/packages/app-bundle/overlay/packages/app/src/components/windows-app-menu.tsx index 1e18af6c..62f9f031 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/windows-app-menu.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/windows-app-menu.tsx @@ -8,7 +8,6 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { useCommand } from "@/context/command" import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu" import { usePlatform } from "@/context/platform" -import { useLanguage } from "@/context/language" export function WindowsAppMenu(props: { command: ReturnType @@ -16,7 +15,6 @@ export function WindowsAppMenu(props: { variant?: "legacy" | "v2" }) { let lastFocused: HTMLElement | undefined - const language = useLanguage() const rememberFocus = () => { const active = document.activeElement @@ -60,7 +58,7 @@ export function WindowsAppMenu(props: { variant="ghost-muted" size="large" icon={} - aria-label={language.t("desktop.menu.ariaLabel")} + aria-label="OpenCode menu" onPointerDown={rememberFocus} onKeyDown={rememberFocus} /> @@ -71,7 +69,7 @@ export function WindowsAppMenu(props: { icon="menu" variant="ghost" class="titlebar-icon rounded-md shrink-0" - aria-label={language.t("desktop.menu.ariaLabel")} + aria-label="OpenCode menu" onPointerDown={rememberFocus} onKeyDown={rememberFocus} /> @@ -81,7 +79,7 @@ export function WindowsAppMenu(props: { OpenCode {DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => ( - + {menu.items ?.filter((entry) => desktopMenuVisible(entry, "windows")) .map((entry) => @@ -89,7 +87,7 @@ export function WindowsAppMenu(props: { ) : ( runEntry(entry)} diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts index c19cdaef..3fb2d82a 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts @@ -35,9 +35,7 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable = new Set(["ar", "ur", "pa", "fa", "dv"]) - -function localeDirection(locale: Locale): Direction { - return RTL_LOCALES.has(locale) ? "rtl" : "ltr" -} + +export type Locale = + | "en" + | "zh" + | "zht" + | "ko" + | "de" + | "es" + | "fr" + | "da" + | "ja" + | "pl" + | "ru" + | "uk" + | "ar" + | "no" + | "br" + | "th" + | "bs" + | "tr" type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten -type PluralKey = - | UiI18nPluralKey - | "session.question.pending" - | "session.followupDock.summary" - | "session.revertDock.summary" type Source = { dict: Record } function cookie(locale: Locale) { return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` } -const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES +const LOCALES: readonly Locale[] = [ + "en", + "zh", + "zht", + "ko", + "de", + "es", + "fr", + "da", + "ja", + "pl", + "ru", + "uk", + "bs", + "ar", + "no", + "br", + "th", + "tr", +] + +const INTL: Record = { + en: "en", + zh: "zh-Hans", + zht: "zh-Hant", + ko: "ko", + de: "de", + es: "es", + fr: "fr", + da: "da", + ja: "ja", + pl: "pl", + ru: "ru", + uk: "uk", + ar: "ar", + no: "nb-NO", + br: "pt-BR", + th: "th", + bs: "bs", + tr: "tr", +} -const INTL = DESKTOP_NATIVE_LOCALE_TAGS +const LABEL_KEY: Record = { + en: "language.en", + zh: "language.zh", + zht: "language.zht", + ko: "language.ko", + de: "language.de", + es: "language.es", + fr: "language.fr", + da: "language.da", + ja: "language.ja", + pl: "language.pl", + ru: "language.ru", + uk: "language.uk", + ar: "language.ar", + no: "language.no", + br: "language.br", + th: "language.th", + bs: "language.bs", + tr: "language.tr", +} const base = i18n.flatten({ ...en, ...uiEn }) const dicts = new Map([["en", base]]) @@ -67,50 +121,6 @@ const loaders: Record, () => Promise> = { th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")), bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")), tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")), - hi: () => merge(import("@/i18n/hi"), import("@opencode-ai/ui/i18n/hi")), - nl: () => merge(import("@/i18n/nl"), import("@opencode-ai/ui/i18n/nl")), - id: () => merge(import("@/i18n/id"), import("@opencode-ai/ui/i18n/id")), - vi: () => merge(import("@/i18n/vi"), import("@opencode-ai/ui/i18n/vi")), - it: () => merge(import("@/i18n/it"), import("@opencode-ai/ui/i18n/it")), - ur: () => merge(import("@/i18n/ur"), import("@opencode-ai/ui/i18n/ur")), - pa: () => merge(import("@/i18n/pa"), import("@opencode-ai/ui/i18n/pa")), - az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")), - fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")), - sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")), - am: () => merge(import("@/i18n/am"), import("@opencode-ai/ui/i18n/am")), - bg: () => merge(import("@/i18n/bg"), import("@opencode-ai/ui/i18n/bg")), - bn: () => merge(import("@/i18n/bn"), import("@opencode-ai/ui/i18n/bn")), - ca: () => merge(import("@/i18n/ca"), import("@opencode-ai/ui/i18n/ca")), - cs: () => merge(import("@/i18n/cs"), import("@opencode-ai/ui/i18n/cs")), - dv: () => merge(import("@/i18n/dv"), import("@opencode-ai/ui/i18n/dv")), - dz: () => merge(import("@/i18n/dz"), import("@opencode-ai/ui/i18n/dz")), - el: () => merge(import("@/i18n/el"), import("@opencode-ai/ui/i18n/el")), - et: () => merge(import("@/i18n/et"), import("@opencode-ai/ui/i18n/et")), - fa: () => merge(import("@/i18n/fa"), import("@opencode-ai/ui/i18n/fa")), - fo: () => merge(import("@/i18n/fo"), import("@opencode-ai/ui/i18n/fo")), - hr: () => merge(import("@/i18n/hr"), import("@opencode-ai/ui/i18n/hr")), - hu: () => merge(import("@/i18n/hu"), import("@opencode-ai/ui/i18n/hu")), - hy: () => merge(import("@/i18n/hy"), import("@opencode-ai/ui/i18n/hy")), - is: () => merge(import("@/i18n/is"), import("@opencode-ai/ui/i18n/is")), - ka: () => merge(import("@/i18n/ka"), import("@opencode-ai/ui/i18n/ka")), - km: () => merge(import("@/i18n/km"), import("@opencode-ai/ui/i18n/km")), - lo: () => merge(import("@/i18n/lo"), import("@opencode-ai/ui/i18n/lo")), - lt: () => merge(import("@/i18n/lt"), import("@opencode-ai/ui/i18n/lt")), - lv: () => merge(import("@/i18n/lv"), import("@opencode-ai/ui/i18n/lv")), - mk: () => merge(import("@/i18n/mk"), import("@opencode-ai/ui/i18n/mk")), - mn: () => merge(import("@/i18n/mn"), import("@opencode-ai/ui/i18n/mn")), - ms: () => merge(import("@/i18n/ms"), import("@opencode-ai/ui/i18n/ms")), - my: () => merge(import("@/i18n/my"), import("@opencode-ai/ui/i18n/my")), - ne: () => merge(import("@/i18n/ne"), import("@opencode-ai/ui/i18n/ne")), - ro: () => merge(import("@/i18n/ro"), import("@opencode-ai/ui/i18n/ro")), - si: () => merge(import("@/i18n/si"), import("@opencode-ai/ui/i18n/si")), - sk: () => merge(import("@/i18n/sk"), import("@opencode-ai/ui/i18n/sk")), - sl: () => merge(import("@/i18n/sl"), import("@opencode-ai/ui/i18n/sl")), - sq: () => merge(import("@/i18n/sq"), import("@opencode-ai/ui/i18n/sq")), - sr: () => merge(import("@/i18n/sr"), import("@opencode-ai/ui/i18n/sr")), - tg: () => merge(import("@/i18n/tg"), import("@opencode-ai/ui/i18n/tg")), - tk: () => merge(import("@/i18n/tk"), import("@opencode-ai/ui/i18n/tk")), - uz: () => merge(import("@/i18n/uz"), import("@opencode-ai/ui/i18n/uz")), } function loadDict(locale: Locale) { @@ -128,9 +138,42 @@ export function loadLocaleDict(locale: Locale) { return loadDict(locale).then(() => undefined) } +const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ + { locale: "en", match: (language) => language.startsWith("en") }, + { locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") }, + { locale: "zh", match: (language) => language.startsWith("zh") }, + { locale: "ko", match: (language) => language.startsWith("ko") }, + { locale: "de", match: (language) => language.startsWith("de") }, + { locale: "es", match: (language) => language.startsWith("es") }, + { locale: "fr", match: (language) => language.startsWith("fr") }, + { locale: "da", match: (language) => language.startsWith("da") }, + { locale: "ja", match: (language) => language.startsWith("ja") }, + { locale: "pl", match: (language) => language.startsWith("pl") }, + { locale: "ru", match: (language) => language.startsWith("ru") }, + { locale: "uk", match: (language) => language.startsWith("uk") }, + { locale: "ar", match: (language) => language.startsWith("ar") }, + { + locale: "no", + match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"), + }, + { locale: "br", match: (language) => language.startsWith("pt") }, + { locale: "th", match: (language) => language.startsWith("th") }, + { locale: "bs", match: (language) => language.startsWith("bs") }, + { locale: "tr", match: (language) => language.startsWith("tr") }, +] + function detectLocale(): Locale { if (typeof navigator !== "object") return "en" - return detectDesktopNativeLocale(navigator.languages?.length ? navigator.languages : [navigator.language]) + + const languages = navigator.languages?.length ? navigator.languages : [navigator.language] + for (const language of languages) { + if (!language) continue + const normalized = language.toLowerCase() + const match = localeMatchers.find((entry) => entry.match(normalized)) + if (match) return match.locale + } + + return "en" } export function normalizeLocale(value: string): Locale { @@ -155,22 +198,12 @@ function readStoredLocale() { // REVERT CONDITION: restore `?? detectLocale()` here and in init() below once // the non-English locales are branded. const warm = readStoredLocale() ?? "en" -const initialLocale = - warm === "en" - ? Promise.resolve(warm) - : loadDict(warm).then( - () => warm, - () => "en" as const, - ) - -export function loadInitialLocale() { - return initialLocale -} +if (warm !== "en") void loadDict(warm) export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ name: "Language", gate: false, - init: (props: { locale?: Locale; onNativeTranslations?: (bundle: DesktopNativeBundle) => void }) => { + init: (props: { locale?: Locale }) => { const initial = props.locale ?? readStoredLocale() ?? "en" // amicode: see comment above `warm` const [store, setStore, _, ready] = persisted( Persist.global("language", ["language.v1"]), @@ -181,13 +214,6 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont const locale = createMemo(() => normalizeLocale(store.locale)) const intl = createMemo(() => INTL[locale()]) - const [layout, setLayout] = createStore({ direction: undefined as Direction | undefined }) - const direction = createMemo(() => layout.direction ?? localeDirection(locale())) - const layoutLocale = createMemo(() => { - if (!layout.direction) return intl() - // Kobalte derives menu direction from locale rather than accepting a direction override. - return layout.direction === "rtl" ? "ar" : "en" - }) const [dict] = createResource(locale, loadDict, { initialValue: dicts.get(initial) ?? base, @@ -198,49 +224,24 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont params?: Record, ) => string - const plural = (key: PluralKey, count: number, params?: Record) => { - const category = pluralCategory(intl(), count) - const current = (dict.loading ? base : (dict() ?? base)) as Record - const candidate = `${key}.${category}` - const fallback = `${key}.other` - return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count }) - } - - const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value] + const label = (value: Locale) => t(LABEL_KEY[value]) createEffect(() => { if (typeof document !== "object") return - const value = locale() - document.documentElement.lang = intl() - document.documentElement.dir = direction() - document.cookie = cookie(value) - }) - - createEffect(() => { - if (!props.onNativeTranslations || dict.loading) return - const current = dict() - if (!current) return - props.onNativeTranslations( - createDesktopNativeBundle(locale(), (key) => current[key] ?? DESKTOP_NATIVE_ENGLISH[key]), - ) + document.documentElement.lang = locale() + document.cookie = cookie(locale()) }) return { ready, locale, intl, - direction, - layoutLocale, locales: LOCALES, label, t, - plural, setLocale(next: Locale) { setStore("locale", normalizeLocale(next)) }, - setDirection(next: Direction) { - setLayout("direction", next === localeDirection(locale()) ? undefined : next) - }, } }, }) diff --git a/packages/app-bundle/overlay/packages/app/src/context/layout.tsx b/packages/app-bundle/overlay/packages/app/src/context/layout.tsx index 3dcc11e1..2a17460f 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/layout.tsx +++ b/packages/app-bundle/overlay/packages/app/src/context/layout.tsx @@ -89,7 +89,7 @@ type TabHandoff = { export type LocalProject = Partial & { worktree: string; expanded: boolean } export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string } -export type ReviewDiffStyle = "unified" | "split" +export type ReviewDiffStyle = "unified" | "split" | "preview" export type ReviewChangeMode = "git" | "branch" | "turn" export type ReviewPanelSource = "context-button" | "other" diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts index 377834c9..f5b871d1 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts @@ -1134,147 +1134,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "ملف", - "desktop.menu.edit": "تحرير", - "desktop.menu.view": "عرض", - "desktop.menu.go": "انتقال", - "desktop.menu.window": "نافذة", - "desktop.menu.help": "مساعدة", - "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...", - "desktop.menu.settings": "الإعدادات", - "desktop.menu.reloadWebview": "إعادة تحميل Webview", - "desktop.menu.restart": "إعادة التشغيل", - "desktop.menu.exportLogs": "تصدير السجلات...", - "desktop.menu.newSession": "جلسة جديدة", - "desktop.menu.openProject": "فتح مشروع...", - "desktop.menu.newWindow": "نافذة جديدة", - "desktop.menu.closeWindow": "إغلاق النافذة", - "desktop.menu.undo": "تراجع", - "desktop.menu.redo": "إعادة", - "desktop.menu.cut": "قص", - "desktop.menu.copy": "نسخ", - "desktop.menu.paste": "لصق", - "desktop.menu.delete": "حذف", - "desktop.menu.selectAll": "تحديد الكل", - "desktop.menu.toggleSidebar": "إظهار الشريط الجانبي أو إخفاؤه", - "desktop.menu.toggleTerminal": "إظهار المحطة الطرفية أو إخفاؤها", - "desktop.menu.toggleFileTree": "إظهار شجرة الملفات أو إخفاؤها", - "desktop.menu.reload": "إعادة التحميل", - "desktop.menu.toggleDeveloperTools": "إظهار أدوات المطور أو إخفاؤها", - "desktop.menu.actualSize": "الحجم الفعلي", - "desktop.menu.zoomIn": "تكبير", - "desktop.menu.zoomOut": "تصغير", - "desktop.menu.toggleFullScreen": "الدخول إلى ملء الشاشة أو الخروج منه", - "desktop.menu.back": "رجوع", - "desktop.menu.forward": "تقدم", - "desktop.menu.previousSession": "الجلسة السابقة", - "desktop.menu.nextSession": "الجلسة التالية", - "desktop.menu.previousProject": "المشروع السابق", - "desktop.menu.nextProject": "المشروع التالي", - "desktop.menu.minimize": "تصغير النافذة", - "desktop.menu.maximize": "تكبير النافذة", - "desktop.menu.documentation": "وثائق OpenCode", - "desktop.menu.supportForum": "منتدى الدعم", - "desktop.menu.shareFeedback": "إرسال الملاحظات", - "desktop.menu.reportBug": "الإبلاغ عن خلل", - "desktop.menu.ariaLabel": "قائمة OpenCode", - "desktop.updater.dialog.checkFailed.message": "فشل التحقق من وجود تحديثات.", - "desktop.updater.dialog.checkFailed.title": "خطأ في التحديث", - "desktop.updater.dialog.upToDate.message": "لديك أحدث إصدار.", - "desktop.updater.dialog.upToDate.title": "لا توجد تحديثات", - "desktop.updater.dialog.ready.message": "تم تنزيل التحديث {{version}}. هل تريد إعادة التشغيل الآن؟", - "desktop.updater.dialog.ready.title": "التحديث جاهز", - "desktop.updater.dialog.restart": "إعادة التشغيل", - "desktop.updater.dialog.later": "لاحقًا", - "desktop.recovery.action.relaunch": "إعادة تشغيل التطبيق", - "desktop.recovery.action.exportLogs": "تصدير السجلات", - "desktop.recovery.action.keepWaiting": "متابعة الانتظار", - "desktop.recovery.action.quit": "إنهاء", - "desktop.recovery.loadFailed": "فشل تحميل OpenCode", - "desktop.recovery.terminated": "توقفت نافذة OpenCode بشكل غير متوقع", - "desktop.recovery.unresponsive": "OpenCode لا يستجيب", - "desktop.recovery.unresponsive.detail": "يمكنك إعادة تشغيل التطبيق أو فتح السجلات أو متابعة الانتظار.", - "desktop.recovery.loadFailed.detail": "النافذة: {{window}}\nعنوان URL: {{url}}\nالخطأ: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "النافذة: {{window}}\nالسبب: {{reason}}\nالرمز: {{code}}", - "desktop.recovery.unknown": "<غير معروف>", - "desktop.dialog.chooseFolder": "اختيار مجلد", - "desktop.dialog.chooseFile": "اختيار ملف", - "desktop.dialog.saveFile": "حفظ الملف", - "desktop.dialog.files": "الملفات", - "desktop.server.local": "الخادم المحلي", - "desktop.wsl.error.windowsOnly": "لا يتوفر WSL إلا على Windows", - "desktop.wsl.error.unavailable": "WSL غير متاح", - "desktop.wsl.error.listInstalled": "فشل سرد توزيعات WSL المثبتة", - "desktop.wsl.error.listOnline": "فشل سرد توزيعات WSL المتاحة عبر الإنترنت", - "desktop.wsl.error.executeDistro": "تعذر تنفيذ الأوامر في التوزيعة", - "desktop.wsl.error.installWsl": "فشل تثبيت WSL", - "desktop.wsl.error.installDistro": "فشل تثبيت التوزيعة: {{distro}}", - "desktop.wsl.error.installOpencode": "فشل تثبيت OpenCode", - "desktop.wsl.error.alreadyAdded": "تمت إضافة {{distro}} بالفعل", - "desktop.wsl.error.opencodeMissing": "opencode غير مثبت في هذه التوزيعة", - "desktop.wsl.error.opencodeCannotRun": "opencode مثبت، لكن تعذر تشغيله", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode غير مثبت في {{distro}}", - "desktop.wsl.error.updateVersion": "اكتمل تحديث OpenCode، لكن {{distro}} ما زالت تعرض الإصدار {{installed}}؛ الإصدار المتوقع هو {{expected}}", - "desktop.wsl.error.noVersion": "لا يوجد إصدار", - "desktop.wsl.error.serverExited": "خرج خادم WSL بعد بدء التشغيل (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "خرج خادم WSL قبل أن يصبح سليمًا (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "انتهت مهلة فحص سلامة العملية المصاحبة للتوزيعة {{distro}} بعد {{timeout}}ms", - "desktop.wsl.error.commandTimeout": "انتهت مهلة {{command}} {{args}} بعد {{timeout}}ms", - "desktop.wsl.error.failedPort": "فشل الحصول على المنفذ", - "desktop.picker.error.notSelected": "لم يحدد منتقي الملفات ملفًا", - "desktop.picker.error.sizeLimit": "تتجاوز المرفقات المحددة الحد البالغ {{limit}} MB", - "command.logs.export": "تصدير السجلات", - "command.session.export": "تصدير الجلسة", - "command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON", - "provider.connect.method.browser": "المتصفح", - "provider.connect.method.headless": "بدون واجهة رسومية", - "provider.custom.unavailable": "الموفرون المخصصون غير متاحين على هذا الخادم", - "help.tabs.toast.ariaLabel": "نقدّم علامات التبويب. نظّم عملك وجلساتك النشطة باستخدام علامات التبويب", - "help.tabs.toast.dismiss": "إغلاق معلومات علامات التبويب", - "help.tabs.title": "نقدّم علامات التبويب", - "help.tabs.description": "نظّم عملك وجلساتك النشطة باستخدام علامات التبويب", - "help.tabs.date": "14 يوليو", - "help.tabs.introduction": "أصبح OpenCode Desktop الآن قائمًا على علامات التبويب.", - "help.tabs.sessions": "ابدأ جلسة جديدة في علامة تبويب، أو افتح جلسة حالية من أي من مشاريعك. افتح علامة تبويب جديدة عندما تبدأ عملاً جديدًا، وأغلقها عند الانتهاء.", - "help.tabs.organize": "يسهّل إبقاء بضع علامات تبويب مفتوحة تنظيم جلساتك النشطة. أعد تسمية علامات التبويب بأسماء يسهل تذكرها إذا كنت تنوي إبقاءها مفتوحة.", - "help.tabs.home": "ستجد جميع جلساتك ومشاريعك في شاشة الصفحة الرئيسية الجديدة. يؤدي تحديد جلسة إلى فتحها في علامة تبويب.", - "help.tabs.persistence": "عندما تعيد فتح التطبيق، تظل علامات تبويبك مفتوحة.", - "help.tabs.worktrees": "لا يدعم التصميم الجديد أشجار عمل Git حتى الآن، وستتوفر قريبًا. لذلك، إذا كنت تفضل مواصلة استخدام التخطيط السابق، فيمكنك التبديل بين التخطيطات في الإعدادات. ضع في اعتبارك أن التخطيط الجديد سيصبح دائمًا خلال بضعة أسابيع.", - "dialog.usageExceeded.dontShowAgain": "عدم الإظهار مرة أخرى", - "context.export.session": "تصدير الجلسة", - "toast.session.export.success.title": "تم تصدير الجلسة", - "toast.session.export.success.description": "تم حفظ الجلسة في ⁨{{filename}}⁩", - "toast.session.export.failed.title": "فشل تصدير الجلسة", - "toast.session.export.failed.description": "حدث خطأ أثناء تصدير الجلسة", - "session.question.pending.zero": "عدد الأسئلة المعلقة: {{count}}", - "session.question.pending.two": "عدد الأسئلة المعلقة: {{count}}", - "session.question.pending.few": "{{count}} أسئلة معلقة", - "session.question.pending.many": "{{count}} سؤالًا معلقًا", - "session.followupDock.summary.zero": "عدد الرسائل في قائمة الانتظار: {{count}}", - "session.followupDock.summary.two": "عدد الرسائل في قائمة الانتظار: {{count}}", - "session.followupDock.summary.few": "{{count}} رسائل في قائمة الانتظار", - "session.followupDock.summary.many": "{{count}} رسالةً في قائمة الانتظار", - "session.revertDock.summary.zero": "عدد الرسائل التي تم التراجع عنها: {{count}}", - "session.revertDock.summary.two": "عدد الرسائل التي تم التراجع عنها: {{count}}", - "session.revertDock.summary.few": "{{count}} رسائل تم التراجع عنها", - "session.revertDock.summary.many": "{{count}} رسالةً تم التراجع عنها", - "terminal.connectTicket.csrfError": "رُفضت تذكرة اتصال PTY بسبب فحوصات الأصل أو CSRF. تحقق من إعداد CORS للخادم.", - "terminal.connectTicket.statusError": "فشلت تذكرة اتصال PTY بالحالة {{status}}", - "titlebar.update": "تحديث", - "titlebar.updateVersion": "تحديث {{version}}", - "common.export": "تصدير", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "فرض أنماط التركيز على جميع العناصر التفاعلية", - "debugBar.focus.on": "تشغيل", - "debugBar.focus.off": "إيقاف", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "فرض اتجاه التخطيط على التطبيق بالكامل دون تغيير اللغة المحددة", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts index 021d196d..8e553fbc 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts @@ -1150,138 +1150,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Arquivo", - "desktop.menu.edit": "Editar", - "desktop.menu.view": "Visualizar", - "desktop.menu.go": "Ir", - "desktop.menu.window": "Janela", - "desktop.menu.help": "Ajuda", - "desktop.menu.checkForUpdates": "Verificar atualizações...", - "desktop.menu.settings": "Configurações", - "desktop.menu.reloadWebview": "Recarregar WebView", - "desktop.menu.restart": "Reiniciar", - "desktop.menu.exportLogs": "Exportar logs...", - "desktop.menu.newSession": "Nova sessão", - "desktop.menu.openProject": "Abrir projeto...", - "desktop.menu.newWindow": "Nova janela", - "desktop.menu.closeWindow": "Fechar janela", - "desktop.menu.undo": "Desfazer", - "desktop.menu.redo": "Refazer", - "desktop.menu.cut": "Recortar", - "desktop.menu.copy": "Copiar", - "desktop.menu.paste": "Colar", - "desktop.menu.delete": "Excluir", - "desktop.menu.selectAll": "Selecionar tudo", - "desktop.menu.toggleSidebar": "Alternar barra lateral", - "desktop.menu.toggleTerminal": "Alternar terminal", - "desktop.menu.toggleFileTree": "Alternar árvore de arquivos", - "desktop.menu.reload": "Recarregar", - "desktop.menu.toggleDeveloperTools": "Alternar ferramentas de desenvolvedor", - "desktop.menu.actualSize": "Tamanho real", - "desktop.menu.zoomIn": "Ampliar", - "desktop.menu.zoomOut": "Reduzir", - "desktop.menu.toggleFullScreen": "Alternar tela inteira", - "desktop.menu.back": "Voltar", - "desktop.menu.forward": "Avançar", - "desktop.menu.previousSession": "Sessão anterior", - "desktop.menu.nextSession": "Próxima sessão", - "desktop.menu.previousProject": "Projeto anterior", - "desktop.menu.nextProject": "Próximo projeto", - "desktop.menu.minimize": "Minimizar", - "desktop.menu.maximize": "Maximizar", - "desktop.menu.documentation": "Documentação do OpenCode", - "desktop.menu.supportForum": "Fórum de suporte", - "desktop.menu.shareFeedback": "Enviar comentários", - "desktop.menu.reportBug": "Relatar um bug", - "desktop.menu.ariaLabel": "Menu do OpenCode", - "desktop.updater.dialog.checkFailed.message": "Falha ao verificar atualizações.", - "desktop.updater.dialog.checkFailed.title": "Erro de atualização", - "desktop.updater.dialog.upToDate.message": "Você está em dia.", - "desktop.updater.dialog.upToDate.title": "Nenhuma atualização", - "desktop.updater.dialog.ready.message": "A atualização {{version}} foi baixada. Deseja reiniciar agora?", - "desktop.updater.dialog.ready.title": "Atualização pronta", - "desktop.updater.dialog.restart": "Reiniciar", - "desktop.updater.dialog.later": "Mais tarde", - "desktop.recovery.action.relaunch": "Reabrir", - "desktop.recovery.action.exportLogs": "Exportar logs", - "desktop.recovery.action.keepWaiting": "Continuar aguardando", - "desktop.recovery.action.quit": "Encerrar", - "desktop.recovery.loadFailed": "Falha ao carregar o OpenCode", - "desktop.recovery.terminated": "A janela do OpenCode foi encerrada inesperadamente", - "desktop.recovery.unresponsive": "O OpenCode não está respondendo", - "desktop.recovery.unresponsive.detail": "Você pode reabrir o aplicativo, abrir os logs ou continuar aguardando.", - "desktop.recovery.loadFailed.detail": "Janela: {{window}}\nURL: {{url}}\nErro: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Janela: {{window}}\nMotivo: {{reason}}\nCódigo: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Escolher uma pasta", - "desktop.dialog.chooseFile": "Escolher um arquivo", - "desktop.dialog.saveFile": "Salvar arquivo", - "desktop.dialog.files": "Arquivos", - "desktop.server.local": "Servidor local", - "desktop.wsl.error.windowsOnly": "O WSL está disponível somente no Windows", - "desktop.wsl.error.unavailable": "O WSL está indisponível", - "desktop.wsl.error.listInstalled": "Falha ao listar as distribuições WSL instaladas", - "desktop.wsl.error.listOnline": "Falha ao listar as distribuições WSL disponíveis online", - "desktop.wsl.error.executeDistro": "Não é possível executar comandos na distribuição", - "desktop.wsl.error.installWsl": "Falha na instalação do WSL", - "desktop.wsl.error.installDistro": "Falha ao instalar a distribuição: {{distro}}", - "desktop.wsl.error.installOpencode": "Falha na instalação do OpenCode", - "desktop.wsl.error.alreadyAdded": "{{distro}} já foi adicionada", - "desktop.wsl.error.opencodeMissing": "O opencode não está instalado nesta distribuição", - "desktop.wsl.error.opencodeCannotRun": "O opencode está instalado, mas não pôde ser executado", - "desktop.wsl.error.opencodeNotInstalled": "O OpenCode não está instalado em {{distro}}", - "desktop.wsl.error.updateVersion": "A atualização do OpenCode foi concluída, mas {{distro}} ainda informa a versão {{installed}}; a versão esperada é {{expected}}", - "desktop.wsl.error.noVersion": "sem versão", - "desktop.wsl.error.serverExited": "O servidor WSL foi encerrado após a inicialização (código={{code}} sinal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "O servidor WSL foi encerrado antes de ficar íntegro (código={{code}} sinal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "O tempo limite da verificação de integridade do sidecar de {{distro}} foi excedido após {{timeout}}ms", - "desktop.wsl.error.commandTimeout": "O tempo limite de {{command}} {{args}} foi excedido após {{timeout}}ms", - "desktop.wsl.error.failedPort": "Falha ao obter a porta", - "desktop.picker.error.notSelected": "O arquivo não foi selecionado pelo seletor", - "desktop.picker.error.sizeLimit": "Os anexos selecionados excedem o limite de {{limit}} MB", - "command.logs.export": "Exportar logs", - "command.session.export": "Exportar sessão", - "command.session.export.description": "Exportar a transcrição completa da sessão como JSON", - "provider.connect.method.browser": "Navegador", - "provider.connect.method.headless": "Sem interface gráfica", - "provider.custom.unavailable": "Provedores personalizados não estão disponíveis neste servidor", - "help.tabs.toast.ariaLabel": "Apresentamos as abas. Organize seu trabalho e suas sessões ativas com abas", - "help.tabs.toast.dismiss": "Dispensar informações sobre as abas", - "help.tabs.title": "Apresentamos as abas", - "help.tabs.description": "Organize seu trabalho e suas sessões ativas com abas", - "help.tabs.date": "14 de julho", - "help.tabs.introduction": "Agora, o OpenCode Desktop é organizado em abas.", - "help.tabs.sessions": "Inicie uma nova sessão em uma aba ou abra uma sessão existente de qualquer um dos seus projetos. Abra uma nova aba quando começar algo novo e feche-a quando terminar.", - "help.tabs.organize": "Manter algumas abas abertas facilita a organização das sessões ativas. Renomeie as abas com nomes fáceis de lembrar se pretende mantê-las abertas.", - "help.tabs.home": "Você encontrará todas as suas sessões e projetos na nova tela Início. Ao selecionar uma sessão, ela será aberta em uma aba.", - "help.tabs.persistence": "Quando você reabrir o aplicativo, suas abas ainda estarão abertas.", - "help.tabs.worktrees": "O novo design ainda não oferece suporte a Git Worktrees, mas esse recurso estará disponível em breve. Se preferir continuar usando o layout anterior, você poderá alternar entre os layouts nas Configurações. Lembre-se apenas de que o novo layout se tornará permanente em algumas semanas.", - "dialog.usageExceeded.dontShowAgain": "Não mostrar novamente", - "context.export.session": "Exportar sessão", - "toast.session.export.success.title": "Sessão exportada", - "toast.session.export.success.description": "Sessão salva em {{filename}}", - "toast.session.export.failed.title": "Falha ao exportar sessão", - "toast.session.export.failed.description": "Ocorreu um erro ao exportar a sessão", - "session.question.pending.many": "{{count}} de perguntas pendentes", - "session.followupDock.summary.many": "{{count}} de mensagens na fila", - "session.revertDock.summary.many": "{{count}} de mensagens revertidas", - "terminal.connectTicket.csrfError": "O tíquete de conexão PTY foi rejeitado pelas verificações de origem ou CSRF. Verifique a configuração de CORS do servidor.", - "terminal.connectTicket.statusError": "O tíquete de conexão PTY falhou com o status {{status}}", - "titlebar.update": "Atualizar", - "titlebar.updateVersion": "Atualizar {{version}}", - "common.export": "Exportar", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Forçar estilos de foco em todos os elementos interativos", - "debugBar.focus.on": "LIGADO", - "debugBar.focus.off": "DESLIGADO", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Forçar a direção do layout de todo o aplicativo sem alterar o idioma selecionado", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts index 1daf0c03..b1da656f 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts @@ -1226,138 +1226,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Datoteka", - "desktop.menu.edit": "Uredi", - "desktop.menu.view": "Prikaz", - "desktop.menu.go": "Idi", - "desktop.menu.window": "Prozor", - "desktop.menu.help": "Pomoć", - "desktop.menu.checkForUpdates": "Provjeri ima li ažuriranja...", - "desktop.menu.settings": "Postavke", - "desktop.menu.reloadWebview": "Ponovo učitaj Webview", - "desktop.menu.restart": "Ponovo pokreni", - "desktop.menu.exportLogs": "Izvezi zapisnike...", - "desktop.menu.newSession": "Nova sesija", - "desktop.menu.openProject": "Otvori projekat...", - "desktop.menu.newWindow": "Novi prozor", - "desktop.menu.closeWindow": "Zatvori prozor", - "desktop.menu.undo": "Poništi", - "desktop.menu.redo": "Vrati", - "desktop.menu.cut": "Izreži", - "desktop.menu.copy": "Kopiraj", - "desktop.menu.paste": "Zalijepi", - "desktop.menu.delete": "Izbriši", - "desktop.menu.selectAll": "Odaberi sve", - "desktop.menu.toggleSidebar": "Prikaži ili sakrij bočnu traku", - "desktop.menu.toggleTerminal": "Prikaži ili sakrij terminal", - "desktop.menu.toggleFileTree": "Prikaži ili sakrij stablo datoteka", - "desktop.menu.reload": "Ponovo učitaj", - "desktop.menu.toggleDeveloperTools": "Prikaži ili sakrij alate za programere", - "desktop.menu.actualSize": "Stvarna veličina", - "desktop.menu.zoomIn": "Uvećaj", - "desktop.menu.zoomOut": "Umanji", - "desktop.menu.toggleFullScreen": "Prikaži ili sakrij prikaz preko cijelog ekrana", - "desktop.menu.back": "Nazad", - "desktop.menu.forward": "Naprijed", - "desktop.menu.previousSession": "Prethodna sesija", - "desktop.menu.nextSession": "Sljedeća sesija", - "desktop.menu.previousProject": "Prethodni projekat", - "desktop.menu.nextProject": "Sljedeći projekat", - "desktop.menu.minimize": "Minimiziraj", - "desktop.menu.maximize": "Maksimiziraj", - "desktop.menu.documentation": "Dokumentacija za OpenCode", - "desktop.menu.supportForum": "Forum za podršku", - "desktop.menu.shareFeedback": "Pošalji povratne informacije", - "desktop.menu.reportBug": "Prijavi grešku", - "desktop.menu.ariaLabel": "OpenCode meni", - "desktop.updater.dialog.checkFailed.message": "Provjera ažuriranja nije uspjela.", - "desktop.updater.dialog.checkFailed.title": "Greška pri ažuriranju", - "desktop.updater.dialog.upToDate.message": "Sve je ažurno.", - "desktop.updater.dialog.upToDate.title": "Nema ažuriranja", - "desktop.updater.dialog.ready.message": "Ažuriranje {{version}} je preuzeto. Želiš li sada ponovo pokrenuti aplikaciju?", - "desktop.updater.dialog.ready.title": "Ažuriranje je spremno", - "desktop.updater.dialog.restart": "Ponovo pokreni", - "desktop.updater.dialog.later": "Kasnije", - "desktop.recovery.action.relaunch": "Ponovo pokreni", - "desktop.recovery.action.exportLogs": "Izvezi zapisnike", - "desktop.recovery.action.keepWaiting": "Nastavi čekati", - "desktop.recovery.action.quit": "Izađi", - "desktop.recovery.loadFailed": "OpenCode se nije uspio učitati", - "desktop.recovery.terminated": "Prozor OpenCode-a neočekivano je zatvoren", - "desktop.recovery.unresponsive": "OpenCode ne reaguje", - "desktop.recovery.unresponsive.detail": "Možeš ponovo pokrenuti aplikaciju, otvoriti zapisnike ili nastaviti čekati.", - "desktop.recovery.loadFailed.detail": "Prozor: {{window}}\nURL: {{url}}\nGreška: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Prozor: {{window}}\nRazlog: {{reason}}\nKod: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Odaberi fasciklu", - "desktop.dialog.chooseFile": "Odaberi datoteku", - "desktop.dialog.saveFile": "Sačuvaj datoteku", - "desktop.dialog.files": "Datoteke", - "desktop.server.local": "Lokalni server", - "desktop.wsl.error.windowsOnly": "WSL je dostupan samo u Windowsu", - "desktop.wsl.error.unavailable": "WSL nije dostupan", - "desktop.wsl.error.listInstalled": "Nije uspjelo prikazivanje instaliranih WSL distribucija", - "desktop.wsl.error.listOnline": "Nije uspjelo prikazivanje dostupnih WSL distribucija", - "desktop.wsl.error.executeDistro": "Nije moguće izvršiti komande u distribuciji", - "desktop.wsl.error.installWsl": "Instalacija WSL-a nije uspjela", - "desktop.wsl.error.installDistro": "Instalacija distribucije nije uspjela: {{distro}}", - "desktop.wsl.error.installOpencode": "Instalacija OpenCode-a nije uspjela", - "desktop.wsl.error.alreadyAdded": "Distribucija {{distro}} je već dodana", - "desktop.wsl.error.opencodeMissing": "opencode nije instaliran u ovoj distribuciji", - "desktop.wsl.error.opencodeCannotRun": "opencode je instaliran, ali se ne može pokrenuti", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode nije instaliran u distribuciji {{distro}}", - "desktop.wsl.error.updateVersion": "Ažuriranje OpenCode-a je završeno, ali {{distro}} i dalje prijavljuje {{installed}}; očekivano je {{expected}}", - "desktop.wsl.error.noVersion": "nema verzije", - "desktop.wsl.error.serverExited": "WSL server se zatvorio nakon pokretanja (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL server se zatvorio prije nego što je postao dostupan (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Isteklo je vrijeme provjere dostupnosti komponente Sidecar za {{distro}} nakon {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "Isteklo je vrijeme za {{command}} {{args}} nakon {{timeout}} ms", - "desktop.wsl.error.failedPort": "Nije uspjelo dohvaćanje porta", - "desktop.picker.error.notSelected": "Datoteka nije odabrana u izborniku", - "desktop.picker.error.sizeLimit": "Odabrani prilozi premašuju ograničenje od {{limit}} MB", - "command.logs.export": "Izvezi zapisnike", - "command.session.export": "Izvezi sesiju", - "command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu", - "provider.connect.method.browser": "Preglednik", - "provider.connect.method.headless": "Bez grafičkog interfejsa", - "provider.custom.unavailable": "Prilagođeni provajderi nisu dostupni na ovom serveru", - "help.tabs.toast.ariaLabel": "Predstavljamo kartice. Organizuj svoj rad i aktivne sesije pomoću kartica", - "help.tabs.toast.dismiss": "Odbaci informacije o karticama", - "help.tabs.title": "Predstavljamo kartice", - "help.tabs.description": "Organizuj svoj rad i aktivne sesije pomoću kartica", - "help.tabs.date": "14. juli", - "help.tabs.introduction": "OpenCode Desktop sada je organizovan oko kartica.", - "help.tabs.sessions": "Započni novu sesiju u kartici ili otvori postojeću sesiju iz bilo kojeg projekta. Otvori novu karticu kada započinješ nešto novo, a zatvori je kada završiš.", - "help.tabs.organize": "Nekoliko otvorenih kartica olakšava organizovanje aktivnih sesija. Preimenuj kartice tako da ih možeš lako prepoznati ako ih namjeravaš zadržati.", - "help.tabs.home": "Sve svoje sesije i projekte pronaći ćeš na novom početnom ekranu. Odabirom sesije ona se otvara u kartici.", - "help.tabs.persistence": "Kada ponovo otvoriš aplikaciju, tvoje kartice ostaju otvorene.", - "help.tabs.worktrees": "Novi dizajn još ne podržava Git Worktrees, ali podrška stiže uskoro. Ako želiš nastaviti koristiti prethodni raspored, možeš mijenjati rasporede u Postavkama. Imaj na umu da će novi raspored za nekoliko sedmica postati trajan.", - "dialog.usageExceeded.dontShowAgain": "Nemoj više prikazivati", - "context.export.session": "Izvezi sesiju", - "toast.session.export.success.title": "Sesija izvezena", - "toast.session.export.success.description": "Sesija je sačuvana kao {{filename}}", - "toast.session.export.failed.title": "Izvoz sesije nije uspio", - "toast.session.export.failed.description": "Došlo je do greške prilikom izvoza sesije", - "session.question.pending.few": "{{count}} pitanja na čekanju", - "session.followupDock.summary.few": "{{count}} poruke na čekanju", - "session.revertDock.summary.few": "{{count}} vraćene poruke", - "terminal.connectTicket.csrfError": "PTY tiket za povezivanje odbijen je zbog provjera porijekla ili CSRF-a. Provjeri CORS konfiguraciju servera.", - "terminal.connectTicket.statusError": "PTY tiket za povezivanje nije uspio sa statusom {{status}}", - "titlebar.update": "Ažuriraj", - "titlebar.updateVersion": "Ažuriraj {{version}}", - "common.export": "Izvezi", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Prisilno primijeni stilove fokusa na sve interaktivne elemente", - "debugBar.focus.on": "UKLJUČENO", - "debugBar.focus.off": "ISKLJUČENO", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Prisilno postavi smjer rasporeda cijele aplikacije bez promjene odabranog jezika", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts index 6c4bfc6a..6c8eccd8 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts @@ -1218,135 +1218,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "command.session.export": "Eksportér session", - "command.session.export.description": "Eksportér hele sessionsudskriften som JSON", - "provider.connect.method.browser": "Browser", - "provider.connect.method.headless": "Browserløs", - "provider.custom.unavailable": "Brugerdefinerede udbydere er ikke tilgængelige på denne server", - "dialog.usageExceeded.dontShowAgain": "Vis ikke igen", - "context.export.session": "Eksportér session", - "toast.session.export.success.title": "Session eksporteret", - "toast.session.export.success.description": "Sessionen blev gemt i {{filename}}", - "toast.session.export.failed.title": "Kunne ikke eksportere session", - "toast.session.export.failed.description": "Der opstod en fejl under eksport af sessionen", - "terminal.connectTicket.csrfError": "PTY-forbindelsesticket blev afvist ved kontrollen af oprindelse eller CSRF. Kontrollér serverens CORS-konfiguration.", - "terminal.connectTicket.statusError": "PTY-forbindelsesticket mislykkedes med {{status}}", - "titlebar.update": "Opdater", - "titlebar.updateVersion": "Opdater {{version}}", - "common.export": "Eksportér", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Gennemtving fokusstile for alle interaktive elementer", - "debugBar.focus.on": "TIL", - "debugBar.focus.off": "FRA", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Gennemtving layoutretningen for hele appen uden at ændre det valgte sprog", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Arkiv", - "desktop.menu.edit": "Rediger", - "desktop.menu.view": "Oversigt", - "desktop.menu.go": "Gå", - "desktop.menu.window": "Vindue", - "desktop.menu.help": "Hjælp", - "desktop.menu.checkForUpdates": "Søg efter opdateringer...", - "desktop.menu.settings": "Indstillinger", - "desktop.menu.reloadWebview": "Genindlæs webvisning", - "desktop.menu.restart": "Genstart", - "desktop.menu.exportLogs": "Eksportér logfiler...", - "desktop.menu.newSession": "Ny session", - "desktop.menu.openProject": "Åbn projekt...", - "desktop.menu.newWindow": "Nyt vindue", - "desktop.menu.closeWindow": "Luk vindue", - "desktop.menu.undo": "Fortryd", - "desktop.menu.redo": "Gentag", - "desktop.menu.cut": "Klip", - "desktop.menu.copy": "Kopier", - "desktop.menu.paste": "Sæt ind", - "desktop.menu.delete": "Slet", - "desktop.menu.selectAll": "Vælg alt", - "desktop.menu.toggleSidebar": "Vis eller skjul indholdsoversigt", - "desktop.menu.toggleTerminal": "Vis eller skjul terminal", - "desktop.menu.toggleFileTree": "Vis eller skjul filtræ", - "desktop.menu.reload": "Genindlæs", - "desktop.menu.toggleDeveloperTools": "Vis eller skjul udviklerværktøjer", - "desktop.menu.actualSize": "Faktisk størrelse", - "desktop.menu.zoomIn": "Zoom ind", - "desktop.menu.zoomOut": "Zoom ud", - "desktop.menu.toggleFullScreen": "Slå fuldskærmsvisning til eller fra", - "desktop.menu.back": "Tilbage", - "desktop.menu.forward": "Frem", - "desktop.menu.previousSession": "Forrige session", - "desktop.menu.nextSession": "Næste session", - "desktop.menu.previousProject": "Forrige projekt", - "desktop.menu.nextProject": "Næste projekt", - "desktop.menu.minimize": "Minimer", - "desktop.menu.maximize": "Maksimer", - "desktop.menu.documentation": "OpenCode-dokumentation", - "desktop.menu.supportForum": "Supportforum", - "desktop.menu.shareFeedback": "Giv feedback", - "desktop.menu.reportBug": "Rapportér en fejl", - "desktop.menu.ariaLabel": "OpenCode-menu", - "desktop.updater.dialog.checkFailed.message": "Søgningen efter opdateringer mislykkedes.", - "desktop.updater.dialog.checkFailed.title": "Opdateringsfejl", - "desktop.updater.dialog.upToDate.message": "Du er opdateret.", - "desktop.updater.dialog.upToDate.title": "Ingen opdateringer", - "desktop.updater.dialog.ready.message": "Opdatering {{version}} er downloadet. Vil du genstarte nu?", - "desktop.updater.dialog.ready.title": "Opdateringen er klar", - "desktop.updater.dialog.restart": "Genstart", - "desktop.updater.dialog.later": "Senere", - "desktop.recovery.action.relaunch": "Start igen", - "desktop.recovery.action.exportLogs": "Eksportér logfiler", - "desktop.recovery.action.keepWaiting": "Vent fortsat", - "desktop.recovery.action.quit": "Afslut", - "desktop.recovery.loadFailed": "OpenCode kunne ikke indlæses", - "desktop.recovery.terminated": "OpenCode-vinduet blev uventet afsluttet", - "desktop.recovery.unresponsive": "OpenCode svarer ikke", - "desktop.recovery.unresponsive.detail": "Du kan starte appen igen, åbne logfilerne eller fortsætte med at vente.", - "desktop.recovery.loadFailed.detail": "Vindue: {{window}}\nURL: {{url}}\nFejl: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Vindue: {{window}}\nÅrsag: {{reason}}\nKode: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Vælg en mappe", - "desktop.dialog.chooseFile": "Vælg en fil", - "desktop.dialog.saveFile": "Gem fil", - "desktop.dialog.files": "Filer", - "desktop.server.local": "Lokal server", - "desktop.wsl.error.windowsOnly": "WSL er kun tilgængelig i Windows", - "desktop.wsl.error.unavailable": "WSL er ikke tilgængelig", - "desktop.wsl.error.listInstalled": "Kunne ikke vise installerede WSL-distributioner", - "desktop.wsl.error.listOnline": "Kunne ikke vise WSL-distributioner på nettet", - "desktop.wsl.error.executeDistro": "Kan ikke udføre kommandoer i distributionen", - "desktop.wsl.error.installWsl": "Installationen af WSL mislykkedes", - "desktop.wsl.error.installDistro": "Kunne ikke installere distributionen: {{distro}}", - "desktop.wsl.error.installOpencode": "Installationen af OpenCode mislykkedes", - "desktop.wsl.error.alreadyAdded": "{{distro}} er allerede tilføjet", - "desktop.wsl.error.opencodeMissing": "opencode er ikke installeret i denne distribution", - "desktop.wsl.error.opencodeCannotRun": "opencode er installeret, men kunne ikke køre", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode er ikke installeret i {{distro}}", - "desktop.wsl.error.updateVersion": "Opdateringen af OpenCode er fuldført, men {{distro}} rapporterer stadig {{installed}}. Forventet: {{expected}}", - "desktop.wsl.error.noVersion": "ingen version", - "desktop.wsl.error.serverExited": "WSL-serveren blev afsluttet efter opstart (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL-serveren blev afsluttet, før den var klar (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Tilstandskontrollen af sidecar-processen for {{distro}} fik timeout efter {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} fik timeout efter {{timeout}} ms", - "desktop.wsl.error.failedPort": "Kunne ikke hente porten", - "desktop.picker.error.notSelected": "Filen blev ikke valgt i filvælgeren", - "desktop.picker.error.sizeLimit": "De valgte vedhæftede filer overskrider grænsen på {{limit}} MB", - "command.logs.export": "Eksportér logfiler", - "help.tabs.toast.ariaLabel": "Introduktion til faner. Organiser dit arbejde og dine aktive sessioner med faner", - "help.tabs.toast.dismiss": "Luk oplysninger om faner", - "help.tabs.title": "Introduktion til faner", - "help.tabs.description": "Organiser dit arbejde og dine aktive sessioner med faner", - "help.tabs.date": "14. juli", - "help.tabs.introduction": "OpenCode Desktop er nu bygget op omkring faner.", - "help.tabs.sessions": "Start en ny session i en fane, eller åbn en eksisterende session fra et af dine projekter. Åbn en ny fane, når du går i gang med noget nyt, og luk den, når du er færdig.", - "help.tabs.organize": "Når du holder nogle få faner åbne, er det nemmere at organisere dine aktive sessioner. Giv fanerne navne, der er nemme at huske, hvis du vil beholde dem.", - "help.tabs.home": "Du finder alle dine sessioner og projekter på den nye startskærm. Når du vælger en session, åbnes den i en fane.", - "help.tabs.persistence": "Dine faner er stadig åbne, når du åbner appen igen.", - "help.tabs.worktrees": "Det nye design understøtter endnu ikke Git Worktrees, men understøttelsen kommer snart. Hvis du foretrækker at fortsætte med det tidligere layout, kan du skifte mellem layoutene under Indstillinger. Bemærk, at det nye layout bliver permanent om nogle få uger.", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts index 8d27221e..0fc55e75 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts @@ -1163,135 +1163,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "command.session.export": "Sitzung exportieren", - "command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren", - "provider.connect.method.browser": "Browser", - "provider.connect.method.headless": "Browserlos", - "provider.custom.unavailable": "Benutzerdefinierte Anbieter sind auf diesem Server nicht verfügbar", - "dialog.usageExceeded.dontShowAgain": "Nicht mehr anzeigen", - "context.export.session": "Sitzung exportieren", - "toast.session.export.success.title": "Sitzung exportiert", - "toast.session.export.success.description": "Sitzung unter {{filename}} gespeichert", - "toast.session.export.failed.title": "Sitzung konnte nicht exportiert werden", - "toast.session.export.failed.description": "Beim Exportieren der Sitzung ist ein Fehler aufgetreten", - "terminal.connectTicket.csrfError": "Das PTY-Verbindungsticket wurde bei der Ursprungs- oder CSRF-Prüfung abgelehnt. Überprüfen Sie die CORS-Konfiguration des Servers.", - "terminal.connectTicket.statusError": "Das PTY-Verbindungsticket ist mit {{status}} fehlgeschlagen", - "titlebar.update": "Aktualisieren", - "titlebar.updateVersion": "Aktualisieren {{version}}", - "common.export": "Exportieren", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Fokusstile für alle interaktiven Elemente erzwingen", - "debugBar.focus.on": "EIN", - "debugBar.focus.off": "AUS", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Layoutrichtung für die gesamte App erzwingen, ohne die ausgewählte Sprache zu ändern", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Datei", - "desktop.menu.edit": "Bearbeiten", - "desktop.menu.view": "Ansicht", - "desktop.menu.go": "Gehe zu", - "desktop.menu.window": "Fenster", - "desktop.menu.help": "Hilfe", - "desktop.menu.checkForUpdates": "Nach Updates suchen…", - "desktop.menu.settings": "Einstellungen", - "desktop.menu.reloadWebview": "Webview neu laden", - "desktop.menu.restart": "Neu starten", - "desktop.menu.exportLogs": "Protokolle exportieren…", - "desktop.menu.newSession": "Neue Sitzung", - "desktop.menu.openProject": "Projekt öffnen…", - "desktop.menu.newWindow": "Neues Fenster", - "desktop.menu.closeWindow": "Fenster schließen", - "desktop.menu.undo": "Rückgängig", - "desktop.menu.redo": "Wiederholen", - "desktop.menu.cut": "Ausschneiden", - "desktop.menu.copy": "Kopieren", - "desktop.menu.paste": "Einfügen", - "desktop.menu.delete": "Löschen", - "desktop.menu.selectAll": "Alles auswählen", - "desktop.menu.toggleSidebar": "Seitenleiste umschalten", - "desktop.menu.toggleTerminal": "Terminal umschalten", - "desktop.menu.toggleFileTree": "Dateibaum umschalten", - "desktop.menu.reload": "Neu laden", - "desktop.menu.toggleDeveloperTools": "Entwicklertools umschalten", - "desktop.menu.actualSize": "Tatsächliche Größe", - "desktop.menu.zoomIn": "Vergrößern", - "desktop.menu.zoomOut": "Verkleinern", - "desktop.menu.toggleFullScreen": "Vollbildmodus umschalten", - "desktop.menu.back": "Zurück", - "desktop.menu.forward": "Vorwärts", - "desktop.menu.previousSession": "Vorherige Sitzung", - "desktop.menu.nextSession": "Nächste Sitzung", - "desktop.menu.previousProject": "Vorheriges Projekt", - "desktop.menu.nextProject": "Nächstes Projekt", - "desktop.menu.minimize": "Minimieren", - "desktop.menu.maximize": "Maximieren", - "desktop.menu.documentation": "OpenCode-Dokumentation", - "desktop.menu.supportForum": "Supportforum", - "desktop.menu.shareFeedback": "Feedback senden", - "desktop.menu.reportBug": "Fehler melden", - "desktop.menu.ariaLabel": "OpenCode-Menü", - "desktop.updater.dialog.checkFailed.message": "Die Suche nach Updates ist fehlgeschlagen.", - "desktop.updater.dialog.checkFailed.title": "Updatefehler", - "desktop.updater.dialog.upToDate.message": "Sie sind auf dem neuesten Stand.", - "desktop.updater.dialog.upToDate.title": "Keine Updates", - "desktop.updater.dialog.ready.message": "Update {{version}} wurde heruntergeladen. Jetzt neu starten?", - "desktop.updater.dialog.ready.title": "Update bereit", - "desktop.updater.dialog.restart": "Neu starten", - "desktop.updater.dialog.later": "Später", - "desktop.recovery.action.relaunch": "Neu starten", - "desktop.recovery.action.exportLogs": "Protokolle exportieren", - "desktop.recovery.action.keepWaiting": "Weiter warten", - "desktop.recovery.action.quit": "Beenden", - "desktop.recovery.loadFailed": "OpenCode konnte nicht geladen werden", - "desktop.recovery.terminated": "Das OpenCode-Fenster wurde unerwartet beendet", - "desktop.recovery.unresponsive": "OpenCode reagiert nicht", - "desktop.recovery.unresponsive.detail": "Sie können die App neu starten, die Protokolle öffnen oder weiter warten.", - "desktop.recovery.loadFailed.detail": "Fenster: {{window}}\nURL: {{url}}\nFehler: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Fenster: {{window}}\nGrund: {{reason}}\nCode: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Ordner auswählen", - "desktop.dialog.chooseFile": "Datei auswählen", - "desktop.dialog.saveFile": "Datei speichern", - "desktop.dialog.files": "Dateien", - "desktop.server.local": "Lokaler Server", - "desktop.wsl.error.windowsOnly": "WSL ist nur unter Windows verfügbar", - "desktop.wsl.error.unavailable": "WSL ist nicht verfügbar", - "desktop.wsl.error.listInstalled": "Installierte WSL-Distributionen konnten nicht aufgelistet werden", - "desktop.wsl.error.listOnline": "Verfügbare WSL-Distributionen konnten nicht aufgelistet werden", - "desktop.wsl.error.executeDistro": "In der Distribution können keine Befehle ausgeführt werden", - "desktop.wsl.error.installWsl": "WSL-Installation fehlgeschlagen", - "desktop.wsl.error.installDistro": "Distribution konnte nicht installiert werden: {{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode-Installation fehlgeschlagen", - "desktop.wsl.error.alreadyAdded": "{{distro}} wurde bereits hinzugefügt", - "desktop.wsl.error.opencodeMissing": "opencode ist in dieser Distribution nicht installiert", - "desktop.wsl.error.opencodeCannotRun": "opencode ist installiert, konnte aber nicht ausgeführt werden", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode ist in {{distro}} nicht installiert", - "desktop.wsl.error.updateVersion": "Das OpenCode-Update wurde abgeschlossen, aber {{distro}} meldet weiterhin {{installed}}; erwartet wurde {{expected}}", - "desktop.wsl.error.noVersion": "keine Version", - "desktop.wsl.error.serverExited": "WSL-Server wurde nach dem Start beendet (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL-Server wurde beendet, bevor er betriebsbereit war (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Zeitüberschreitung bei der Integritätsprüfung des Sidecars für {{distro}} nach {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "Zeitüberschreitung bei {{command}} {{args}} nach {{timeout}} ms", - "desktop.wsl.error.failedPort": "Port konnte nicht abgerufen werden", - "desktop.picker.error.notSelected": "Im Auswahldialog wurde keine Datei ausgewählt", - "desktop.picker.error.sizeLimit": "Die ausgewählten Anhänge überschreiten das Limit von {{limit}} MB", - "command.logs.export": "Protokolle exportieren", - "help.tabs.toast.ariaLabel": "Neu: Tabs. Organisieren Sie Ihre Arbeit und aktiven Sitzungen mithilfe von Tabs", - "help.tabs.toast.dismiss": "Informationen zu Tabs schließen", - "help.tabs.title": "Neu: Tabs", - "help.tabs.description": "Organisieren Sie Ihre Arbeit und aktiven Sitzungen mithilfe von Tabs", - "help.tabs.date": "14. Juli", - "help.tabs.introduction": "OpenCode Desktop basiert jetzt auf Tabs.", - "help.tabs.sessions": "Starten Sie eine neue Sitzung in einem Tab oder öffnen Sie eine vorhandene Sitzung aus einem Ihrer Projekte. Öffnen Sie einen neuen Tab, wenn Sie etwas Neues beginnen, und schließen Sie ihn, wenn Sie fertig sind.", - "help.tabs.organize": "Wenn Sie einige Tabs geöffnet lassen, können Sie Ihre aktiven Sitzungen leichter organisieren. Benennen Sie Tabs einprägsam, wenn Sie sie länger behalten möchten.", - "help.tabs.home": "Auf der neuen Startseite finden Sie alle Ihre Sitzungen und Projekte. Wenn Sie eine Sitzung auswählen, wird sie in einem Tab geöffnet.", - "help.tabs.persistence": "Wenn Sie die App erneut öffnen, sind Ihre Tabs weiterhin geöffnet.", - "help.tabs.worktrees": "Das neue Design unterstützt Git-Worktrees noch nicht. Diese Funktion folgt in Kürze. Wenn Sie das bisherige Layout vorerst weiterverwenden möchten, können Sie in den Einstellungen zwischen den Layouts wechseln. Beachten Sie jedoch, dass das neue Layout in einigen Wochen dauerhaft verwendet wird.", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts index b639004a..32dcd9b0 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts @@ -1235,133 +1235,4 @@ export const dict = { "workspace.reset.archived.one": "1 session will be archived.", "workspace.reset.archived.many": "{{count}} sessions will be archived.", "workspace.reset.note": "This will reset the workspace to match the default branch.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // absent from the fork-era branded dict. English values verbatim. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "File", - "desktop.menu.edit": "Edit", - "desktop.menu.view": "View", - "desktop.menu.go": "Go", - "desktop.menu.window": "Window", - "desktop.menu.help": "Help", - "desktop.menu.checkForUpdates": "Check for Updates...", - "desktop.menu.settings": "Settings", - "desktop.menu.reloadWebview": "Reload Webview", - "desktop.menu.restart": "Restart", - "desktop.menu.exportLogs": "Export Logs...", - "desktop.menu.newSession": "New Session", - "desktop.menu.openProject": "Open Project...", - "desktop.menu.newWindow": "New Window", - "desktop.menu.closeWindow": "Close Window", - "desktop.menu.undo": "Undo", - "desktop.menu.redo": "Redo", - "desktop.menu.cut": "Cut", - "desktop.menu.copy": "Copy", - "desktop.menu.paste": "Paste", - "desktop.menu.delete": "Delete", - "desktop.menu.selectAll": "Select All", - "desktop.menu.toggleSidebar": "Toggle Sidebar", - "desktop.menu.toggleTerminal": "Toggle Terminal", - "desktop.menu.toggleFileTree": "Toggle File Tree", - "desktop.menu.reload": "Reload", - "desktop.menu.toggleDeveloperTools": "Toggle Developer Tools", - "desktop.menu.actualSize": "Actual Size", - "desktop.menu.zoomIn": "Zoom In", - "desktop.menu.zoomOut": "Zoom Out", - "desktop.menu.toggleFullScreen": "Toggle Full Screen", - "desktop.menu.back": "Back", - "desktop.menu.forward": "Forward", - "desktop.menu.previousSession": "Previous Session", - "desktop.menu.nextSession": "Next Session", - "desktop.menu.previousProject": "Previous Project", - "desktop.menu.nextProject": "Next Project", - "desktop.menu.minimize": "Minimize", - "desktop.menu.maximize": "Maximize", - "desktop.menu.documentation": "OpenCode Documentation", - "desktop.menu.supportForum": "Support Forum", - "desktop.menu.shareFeedback": "Share Feedback", - "desktop.menu.reportBug": "Report a Bug", - "desktop.menu.ariaLabel": "OpenCode menu", - "desktop.updater.dialog.checkFailed.message": "Update check failed.", - "desktop.updater.dialog.checkFailed.title": "Update Error", - "desktop.updater.dialog.upToDate.message": "You're up to date.", - "desktop.updater.dialog.upToDate.title": "No Updates", - "desktop.updater.dialog.ready.message": "Update {{version}} downloaded. Restart now?", - "desktop.updater.dialog.ready.title": "Update Ready", - "desktop.updater.dialog.restart": "Restart", - "desktop.updater.dialog.later": "Later", - "desktop.recovery.action.relaunch": "Relaunch", - "desktop.recovery.action.exportLogs": "Export Logs", - "desktop.recovery.action.keepWaiting": "Keep Waiting", - "desktop.recovery.action.quit": "Quit", - "desktop.recovery.loadFailed": "OpenCode failed to load", - "desktop.recovery.terminated": "OpenCode window terminated unexpectedly", - "desktop.recovery.unresponsive": "OpenCode is not responding", - "desktop.recovery.unresponsive.detail": "You can relaunch the app, open the logs, or keep waiting.", - "desktop.recovery.loadFailed.detail": "Window: {{window}}\nURL: {{url}}\nError: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Window: {{window}}\nReason: {{reason}}\nCode: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Choose a folder", - "desktop.dialog.chooseFile": "Choose a file", - "desktop.dialog.saveFile": "Save file", - "desktop.dialog.files": "Files", - "desktop.server.local": "Local Server", - "desktop.wsl.error.windowsOnly": "WSL is only available on Windows", - "desktop.wsl.error.unavailable": "WSL is unavailable", - "desktop.wsl.error.listInstalled": "Failed to list installed WSL distros", - "desktop.wsl.error.listOnline": "Failed to list online WSL distros", - "desktop.wsl.error.executeDistro": "Cannot execute commands in distro", - "desktop.wsl.error.installWsl": "WSL installation failed", - "desktop.wsl.error.installDistro": "Failed to install distro: {{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode installation failed", - "desktop.wsl.error.alreadyAdded": "{{distro}} is already added", - "desktop.wsl.error.opencodeMissing": "opencode is not installed in this distro", - "desktop.wsl.error.opencodeCannotRun": "opencode is installed but could not run", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode is not installed in {{distro}}", - "desktop.wsl.error.updateVersion": "OpenCode update finished but {{distro}} still reports {{installed}}; expected {{expected}}", - "desktop.wsl.error.noVersion": "no version", - "desktop.wsl.error.serverExited": "WSL server exited after startup (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL server exited before becoming healthy (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Sidecar for {{distro}} health check timed out after {{timeout}}ms", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} timed out after {{timeout}}ms", - "desktop.wsl.error.failedPort": "Failed to get port", - "desktop.picker.error.notSelected": "File was not selected by the picker", - "desktop.picker.error.sizeLimit": "Selected attachments exceed the {{limit}} MB limit", - "command.logs.export": "Export logs", - "command.session.export": "Export session", - "command.session.export.description": "Export the full session transcript as JSON", - "provider.connect.method.browser": "Browser", - "provider.connect.method.headless": "Headless", - "provider.custom.unavailable": "Custom providers are unavailable on this server", - "help.tabs.toast.ariaLabel": "Introducing Tabs. Organize your work and active sessions with tabs", - "help.tabs.toast.dismiss": "Dismiss Tabs information", - "help.tabs.title": "Introducing Tabs", - "help.tabs.description": "Organize your work and active sessions with tabs", - "help.tabs.date": "July 14", - "help.tabs.introduction": "OpenCode Desktop is now built around tabs.", - "help.tabs.sessions": "Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when you're starting something new, and close it when you're done.", - "help.tabs.organize": "Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something memorable if you plan to keep them around.", - "help.tabs.home": "You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab.", - "help.tabs.persistence": "When you reopen the app, your tabs are still open.", - "help.tabs.worktrees": "The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout will become permanent in a few weeks.", - "dialog.usageExceeded.dontShowAgain": "Don't show again", - "context.export.session": "Export session", - "toast.session.export.success.title": "Session exported", - "toast.session.export.success.description": "Saved session to {{filename}}", - "toast.session.export.failed.title": "Failed to export session", - "toast.session.export.failed.description": "An error occurred while exporting the session", - "terminal.connectTicket.csrfError": "PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.", - "terminal.connectTicket.statusError": "PTY connect ticket failed with {{status}}", - "titlebar.update": "Update", - "titlebar.updateVersion": "Update {{version}}", - "common.export": "Export", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Force focus styles on all interactive elements", - "debugBar.focus.on": "ON", - "debugBar.focus.off": "OFF", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Force the full app layout direction without changing the selected language", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts index 5950aab0..72f3152a 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts @@ -1234,138 +1234,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Archivo", - "desktop.menu.edit": "Editar", - "desktop.menu.view": "Ver", - "desktop.menu.go": "Ir", - "desktop.menu.window": "Ventana", - "desktop.menu.help": "Ayuda", - "desktop.menu.checkForUpdates": "Buscar actualizaciones...", - "desktop.menu.settings": "Ajustes", - "desktop.menu.reloadWebview": "Volver a cargar la vista web", - "desktop.menu.restart": "Reiniciar", - "desktop.menu.exportLogs": "Exportar registros...", - "desktop.menu.newSession": "Nueva sesión", - "desktop.menu.openProject": "Abrir proyecto...", - "desktop.menu.newWindow": "Nueva ventana", - "desktop.menu.closeWindow": "Cerrar ventana", - "desktop.menu.undo": "Deshacer", - "desktop.menu.redo": "Rehacer", - "desktop.menu.cut": "Cortar", - "desktop.menu.copy": "Copiar", - "desktop.menu.paste": "Pegar", - "desktop.menu.delete": "Eliminar", - "desktop.menu.selectAll": "Seleccionar todo", - "desktop.menu.toggleSidebar": "Mostrar u ocultar barra lateral", - "desktop.menu.toggleTerminal": "Mostrar u ocultar terminal", - "desktop.menu.toggleFileTree": "Mostrar u ocultar árbol de archivos", - "desktop.menu.reload": "Volver a cargar", - "desktop.menu.toggleDeveloperTools": "Mostrar u ocultar herramientas para desarrolladores", - "desktop.menu.actualSize": "Tamaño real", - "desktop.menu.zoomIn": "Ampliar", - "desktop.menu.zoomOut": "Reducir", - "desktop.menu.toggleFullScreen": "Mostrar u ocultar pantalla completa", - "desktop.menu.back": "Atrás", - "desktop.menu.forward": "Adelante", - "desktop.menu.previousSession": "Sesión anterior", - "desktop.menu.nextSession": "Siguiente sesión", - "desktop.menu.previousProject": "Proyecto anterior", - "desktop.menu.nextProject": "Siguiente proyecto", - "desktop.menu.minimize": "Minimizar", - "desktop.menu.maximize": "Maximizar", - "desktop.menu.documentation": "Documentación de OpenCode", - "desktop.menu.supportForum": "Foro de asistencia", - "desktop.menu.shareFeedback": "Enviar comentarios", - "desktop.menu.reportBug": "Informar de un error", - "desktop.menu.ariaLabel": "Menú de OpenCode", - "desktop.updater.dialog.checkFailed.message": "No se pudo buscar actualizaciones.", - "desktop.updater.dialog.checkFailed.title": "Error de actualización", - "desktop.updater.dialog.upToDate.message": "Estás al día.", - "desktop.updater.dialog.upToDate.title": "No hay actualizaciones", - "desktop.updater.dialog.ready.message": "Se ha descargado la actualización {{version}}. ¿Quieres reiniciar ahora?", - "desktop.updater.dialog.ready.title": "Actualización lista", - "desktop.updater.dialog.restart": "Reiniciar", - "desktop.updater.dialog.later": "Más tarde", - "desktop.recovery.action.relaunch": "Volver a iniciar", - "desktop.recovery.action.exportLogs": "Exportar registros", - "desktop.recovery.action.keepWaiting": "Seguir esperando", - "desktop.recovery.action.quit": "Salir", - "desktop.recovery.loadFailed": "No se pudo cargar OpenCode", - "desktop.recovery.terminated": "La ventana de OpenCode se cerró de forma inesperada", - "desktop.recovery.unresponsive": "OpenCode no responde", - "desktop.recovery.unresponsive.detail": "Puedes volver a iniciar la aplicación, abrir los registros o seguir esperando.", - "desktop.recovery.loadFailed.detail": "Ventana: {{window}}\nURL: {{url}}\nError: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Ventana: {{window}}\nMotivo: {{reason}}\nCódigo: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Elegir una carpeta", - "desktop.dialog.chooseFile": "Elegir un archivo", - "desktop.dialog.saveFile": "Guardar archivo", - "desktop.dialog.files": "Archivos", - "desktop.server.local": "Servidor local", - "desktop.wsl.error.windowsOnly": "WSL solo está disponible en Windows", - "desktop.wsl.error.unavailable": "WSL no está disponible", - "desktop.wsl.error.listInstalled": "No se pudieron listar las distribuciones WSL instaladas", - "desktop.wsl.error.listOnline": "No se pudieron listar las distribuciones WSL en línea", - "desktop.wsl.error.executeDistro": "No se pueden ejecutar comandos en la distribución", - "desktop.wsl.error.installWsl": "No se pudo instalar WSL", - "desktop.wsl.error.installDistro": "No se pudo instalar la distribución: {{distro}}", - "desktop.wsl.error.installOpencode": "No se pudo instalar OpenCode", - "desktop.wsl.error.alreadyAdded": "Ya se ha añadido {{distro}}", - "desktop.wsl.error.opencodeMissing": "opencode no está instalado en esta distribución", - "desktop.wsl.error.opencodeCannotRun": "opencode está instalado, pero no se pudo ejecutar", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode no está instalado en {{distro}}", - "desktop.wsl.error.updateVersion": "La actualización de OpenCode finalizó, pero {{distro}} sigue indicando {{installed}}; se esperaba {{expected}}", - "desktop.wsl.error.noVersion": "sin versión", - "desktop.wsl.error.serverExited": "El servidor WSL se cerró después de iniciarse (código={{code}} señal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "El servidor WSL se cerró antes de estar operativo (código={{code}} señal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Se agotó el tiempo de espera de la comprobación de estado del sidecar de {{distro}} tras {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "Se agotó el tiempo de espera de {{command}} {{args}} tras {{timeout}} ms", - "desktop.wsl.error.failedPort": "No se pudo obtener el puerto", - "desktop.picker.error.notSelected": "No se seleccionó ningún archivo en el selector", - "desktop.picker.error.sizeLimit": "Los adjuntos seleccionados superan el límite de {{limit}} MB", - "command.logs.export": "Exportar registros", - "command.session.export": "Exportar sesión", - "command.session.export.description": "Exportar la transcripción completa de la sesión como JSON", - "provider.connect.method.browser": "Navegador", - "provider.connect.method.headless": "Sin navegador", - "provider.custom.unavailable": "Los proveedores personalizados no están disponibles en este servidor", - "help.tabs.toast.ariaLabel": "Presentamos las pestañas. Organiza tu trabajo y las sesiones activas con pestañas", - "help.tabs.toast.dismiss": "Descartar información sobre las pestañas", - "help.tabs.title": "Presentamos las pestañas", - "help.tabs.description": "Organiza tu trabajo y las sesiones activas con pestañas", - "help.tabs.date": "14 de julio", - "help.tabs.introduction": "OpenCode Desktop ahora se organiza en torno a pestañas.", - "help.tabs.sessions": "Inicia una nueva sesión en una pestaña o abre una sesión existente de cualquiera de tus proyectos. Abre una pestaña nueva cuando empieces algo nuevo y ciérrala cuando termines.", - "help.tabs.organize": "Mantener algunas pestañas abiertas facilita la organización de las sesiones activas. Cambia el nombre de las pestañas por uno fácil de recordar si piensas conservarlas.", - "help.tabs.home": "Encontrarás todas tus sesiones y proyectos en la nueva pantalla Inicio. Al seleccionar una sesión, se abre en una pestaña.", - "help.tabs.persistence": "Cuando vuelvas a abrir la aplicación, las pestañas seguirán abiertas.", - "help.tabs.worktrees": "El nuevo diseño aún no admite Git Worktrees, pero estarán disponibles pronto. Si prefieres seguir usando el diseño anterior, puedes cambiar de diseño en Ajustes. Ten en cuenta que el nuevo diseño será permanente dentro de unas semanas.", - "dialog.usageExceeded.dontShowAgain": "No volver a mostrar", - "context.export.session": "Exportar sesión", - "toast.session.export.success.title": "Sesión exportada", - "toast.session.export.success.description": "Sesión guardada en {{filename}}", - "toast.session.export.failed.title": "No se pudo exportar la sesión", - "toast.session.export.failed.description": "Se produjo un error al exportar la sesión", - "session.question.pending.many": "{{count}} de preguntas pendientes", - "session.followupDock.summary.many": "{{count}} de mensajes en cola", - "session.revertDock.summary.many": "{{count}} de mensajes revertidos", - "terminal.connectTicket.csrfError": "El ticket de conexión PTY fue rechazado por las comprobaciones de origen o CSRF. Comprueba la configuración CORS del servidor.", - "terminal.connectTicket.statusError": "El ticket de conexión PTY falló con {{status}}", - "titlebar.update": "Actualizar", - "titlebar.updateVersion": "Actualizar {{version}}", - "common.export": "Exportar", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Forzar los estilos de foco en todos los elementos interactivos", - "debugBar.focus.on": "SÍ", - "debugBar.focus.off": "NO", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Forzar la dirección del diseño de toda la aplicación sin cambiar el idioma seleccionado", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts index 94b632a8..ee47be36 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts @@ -1163,138 +1163,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Fichier", - "desktop.menu.edit": "Édition", - "desktop.menu.view": "Affichage", - "desktop.menu.go": "Aller", - "desktop.menu.window": "Fenêtre", - "desktop.menu.help": "Aide", - "desktop.menu.checkForUpdates": "Rechercher les mises à jour...", - "desktop.menu.settings": "Paramètres", - "desktop.menu.reloadWebview": "Recharger la vue Web", - "desktop.menu.restart": "Redémarrer", - "desktop.menu.exportLogs": "Exporter les journaux...", - "desktop.menu.newSession": "Nouvelle session", - "desktop.menu.openProject": "Ouvrir un projet...", - "desktop.menu.newWindow": "Nouvelle fenêtre", - "desktop.menu.closeWindow": "Fermer la fenêtre", - "desktop.menu.undo": "Annuler", - "desktop.menu.redo": "Rétablir", - "desktop.menu.cut": "Couper", - "desktop.menu.copy": "Copier", - "desktop.menu.paste": "Coller", - "desktop.menu.delete": "Supprimer", - "desktop.menu.selectAll": "Tout sélectionner", - "desktop.menu.toggleSidebar": "Afficher ou masquer la barre latérale", - "desktop.menu.toggleTerminal": "Afficher ou masquer le terminal", - "desktop.menu.toggleFileTree": "Afficher ou masquer l'arborescence des fichiers", - "desktop.menu.reload": "Recharger", - "desktop.menu.toggleDeveloperTools": "Afficher ou masquer les outils de développement", - "desktop.menu.actualSize": "Taille réelle", - "desktop.menu.zoomIn": "Zoom avant", - "desktop.menu.zoomOut": "Zoom arrière", - "desktop.menu.toggleFullScreen": "Activer ou désactiver le mode plein écran", - "desktop.menu.back": "Précédent", - "desktop.menu.forward": "Suivant", - "desktop.menu.previousSession": "Session précédente", - "desktop.menu.nextSession": "Session suivante", - "desktop.menu.previousProject": "Projet précédent", - "desktop.menu.nextProject": "Projet suivant", - "desktop.menu.minimize": "Réduire", - "desktop.menu.maximize": "Agrandir", - "desktop.menu.documentation": "Documentation d'OpenCode", - "desktop.menu.supportForum": "Forum d'assistance", - "desktop.menu.shareFeedback": "Envoyer des commentaires", - "desktop.menu.reportBug": "Signaler un bogue", - "desktop.menu.ariaLabel": "Menu OpenCode", - "desktop.updater.dialog.checkFailed.message": "La recherche de mises à jour a échoué.", - "desktop.updater.dialog.checkFailed.title": "Erreur de mise à jour", - "desktop.updater.dialog.upToDate.message": "Vous êtes à jour.", - "desktop.updater.dialog.upToDate.title": "Aucune mise à jour", - "desktop.updater.dialog.ready.message": "La mise à jour {{version}} a été téléchargée. Redémarrer maintenant ?", - "desktop.updater.dialog.ready.title": "Mise à jour prête", - "desktop.updater.dialog.restart": "Redémarrer", - "desktop.updater.dialog.later": "Plus tard", - "desktop.recovery.action.relaunch": "Relancer", - "desktop.recovery.action.exportLogs": "Exporter les journaux", - "desktop.recovery.action.keepWaiting": "Continuer d'attendre", - "desktop.recovery.action.quit": "Quitter", - "desktop.recovery.loadFailed": "Échec du chargement d'OpenCode", - "desktop.recovery.terminated": "La fenêtre OpenCode s'est arrêtée de manière inattendue", - "desktop.recovery.unresponsive": "OpenCode ne répond pas", - "desktop.recovery.unresponsive.detail": "Vous pouvez relancer l'application, ouvrir les journaux ou continuer d'attendre.", - "desktop.recovery.loadFailed.detail": "Fenêtre : {{window}}\nURL : {{url}}\nErreur : {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Fenêtre : {{window}}\nMotif : {{reason}}\nCode : {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Sélectionner un dossier", - "desktop.dialog.chooseFile": "Sélectionner un fichier", - "desktop.dialog.saveFile": "Enregistrer le fichier", - "desktop.dialog.files": "Fichiers", - "desktop.server.local": "Serveur local", - "desktop.wsl.error.windowsOnly": "WSL est disponible uniquement sous Windows", - "desktop.wsl.error.unavailable": "WSL est indisponible", - "desktop.wsl.error.listInstalled": "Impossible de répertorier les distributions WSL installées", - "desktop.wsl.error.listOnline": "Impossible de répertorier les distributions WSL en ligne", - "desktop.wsl.error.executeDistro": "Impossible d'exécuter des commandes dans la distribution", - "desktop.wsl.error.installWsl": "Échec de l'installation de WSL", - "desktop.wsl.error.installDistro": "Échec de l'installation de la distribution : {{distro}}", - "desktop.wsl.error.installOpencode": "Échec de l'installation d'OpenCode", - "desktop.wsl.error.alreadyAdded": "{{distro}} a déjà été ajoutée", - "desktop.wsl.error.opencodeMissing": "opencode n'est pas installé dans cette distribution", - "desktop.wsl.error.opencodeCannotRun": "opencode est installé, mais n'a pas pu s'exécuter", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode n'est pas installé dans {{distro}}", - "desktop.wsl.error.updateVersion": "La mise à jour d'OpenCode est terminée, mais {{distro}} indique toujours {{installed}} au lieu de {{expected}}", - "desktop.wsl.error.noVersion": "aucune version", - "desktop.wsl.error.serverExited": "Le serveur WSL s'est arrêté après son démarrage (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "Le serveur WSL s'est arrêté avant d'être opérationnel (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "La vérification de l'état du processus auxiliaire pour {{distro}} a dépassé le délai de {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} a dépassé le délai de {{timeout}} ms", - "desktop.wsl.error.failedPort": "Impossible d'obtenir le port", - "desktop.picker.error.notSelected": "Aucun fichier n'a été sélectionné dans le sélecteur", - "desktop.picker.error.sizeLimit": "Les pièces jointes sélectionnées dépassent la limite de {{limit}} Mo", - "command.logs.export": "Exporter les journaux", - "command.session.export": "Exporter la session", - "command.session.export.description": "Exporter la transcription intégrale de la session au format JSON", - "provider.connect.method.browser": "Navigateur", - "provider.connect.method.headless": "Mode headless", - "provider.custom.unavailable": "Les fournisseurs personnalisés ne sont pas disponibles sur ce serveur", - "help.tabs.toast.ariaLabel": "Présentation des onglets. Organisez votre travail et vos sessions actives à l'aide d'onglets", - "help.tabs.toast.dismiss": "Ignorer les informations sur les onglets", - "help.tabs.title": "Présentation des onglets", - "help.tabs.description": "Organisez votre travail et vos sessions actives à l'aide d'onglets", - "help.tabs.date": "14 juillet", - "help.tabs.introduction": "OpenCode Desktop s'articule désormais autour d'onglets.", - "help.tabs.sessions": "Démarrez une nouvelle session dans un onglet ou ouvrez une session existante depuis l'un de vos projets. Ouvrez un nouvel onglet lorsque vous commencez une nouvelle tâche et fermez-le une fois celle-ci terminée.", - "help.tabs.organize": "Garder quelques onglets ouverts facilite l'organisation de vos sessions actives. Si vous comptez les conserver, renommez-les pour pouvoir les reconnaître facilement.", - "help.tabs.home": "Vous trouverez toutes vos sessions et tous vos projets sur le nouvel écran d'accueil. La sélection d'une session l'ouvre dans un onglet.", - "help.tabs.persistence": "Lorsque vous rouvrez l'application, vos onglets sont toujours ouverts.", - "help.tabs.worktrees": "La nouvelle interface ne prend pas encore en charge les arbres de travail Git, mais cette fonctionnalité sera bientôt disponible. Si vous préférez continuer à utiliser la mise en page précédente, vous pouvez changer de mise en page dans les paramètres. N'oubliez pas que la nouvelle mise en page deviendra définitive dans quelques semaines.", - "dialog.usageExceeded.dontShowAgain": "Ne plus afficher", - "context.export.session": "Exporter la session", - "toast.session.export.success.title": "Session exportée", - "toast.session.export.success.description": "Session enregistrée dans {{filename}}", - "toast.session.export.failed.title": "Échec de l’exportation de la session", - "toast.session.export.failed.description": "Une erreur s’est produite lors de l’exportation de la session", - "session.question.pending.many": "{{count}} de questions en attente", - "session.followupDock.summary.many": "{{count}} de messages en file d'attente", - "session.revertDock.summary.many": "{{count}} de messages annulés", - "common.export": "Exporter", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Forcer les styles de focus sur tous les éléments interactifs", - "debugBar.focus.on": "ACTIVÉ", - "debugBar.focus.off": "DÉSACTIVÉ", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Forcer le sens de disposition de toute l’application sans changer la langue sélectionnée", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "terminal.connectTicket.csrfError": "Le ticket de connexion PTY a été rejeté par les contrôles d'origine ou CSRF. Vérifiez la configuration CORS du serveur.", - "terminal.connectTicket.statusError": "Le ticket de connexion PTY a échoué avec le statut {{status}}", - "titlebar.update": "Mettre à jour", - "titlebar.updateVersion": "Mettre à jour {{version}}", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts index 1cd65a97..5789239e 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts @@ -1142,135 +1142,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "ファイル", - "desktop.menu.edit": "編集", - "desktop.menu.view": "表示", - "desktop.menu.go": "移動", - "desktop.menu.window": "ウインドウ", - "desktop.menu.help": "ヘルプ", - "desktop.menu.checkForUpdates": "アップデートを確認...", - "desktop.menu.settings": "設定", - "desktop.menu.reloadWebview": "Webビューを再読み込み", - "desktop.menu.restart": "再起動", - "desktop.menu.exportLogs": "ログをエクスポート...", - "desktop.menu.newSession": "新しいセッション", - "desktop.menu.openProject": "プロジェクトを開く...", - "desktop.menu.newWindow": "新しいウインドウ", - "desktop.menu.closeWindow": "ウインドウを閉じる", - "desktop.menu.undo": "取り消す", - "desktop.menu.redo": "やり直す", - "desktop.menu.cut": "切り取り", - "desktop.menu.copy": "コピー", - "desktop.menu.paste": "ペースト", - "desktop.menu.delete": "削除", - "desktop.menu.selectAll": "すべてを選択", - "desktop.menu.toggleSidebar": "サイドバーの表示を切り替える", - "desktop.menu.toggleTerminal": "ターミナルの表示を切り替える", - "desktop.menu.toggleFileTree": "ファイルツリーの表示を切り替える", - "desktop.menu.reload": "再読み込み", - "desktop.menu.toggleDeveloperTools": "開発者ツールの表示を切り替える", - "desktop.menu.actualSize": "実際のサイズ", - "desktop.menu.zoomIn": "拡大", - "desktop.menu.zoomOut": "縮小", - "desktop.menu.toggleFullScreen": "フルスクリーン表示を切り替える", - "desktop.menu.back": "戻る", - "desktop.menu.forward": "進む", - "desktop.menu.previousSession": "前のセッション", - "desktop.menu.nextSession": "次のセッション", - "desktop.menu.previousProject": "前のプロジェクト", - "desktop.menu.nextProject": "次のプロジェクト", - "desktop.menu.minimize": "最小化", - "desktop.menu.maximize": "最大化", - "desktop.menu.documentation": "OpenCodeドキュメント", - "desktop.menu.supportForum": "サポートフォーラム", - "desktop.menu.shareFeedback": "フィードバックを送信", - "desktop.menu.reportBug": "バグを報告", - "desktop.menu.ariaLabel": "OpenCodeメニュー", - "desktop.updater.dialog.checkFailed.message": "アップデートを確認できませんでした。", - "desktop.updater.dialog.checkFailed.title": "アップデートエラー", - "desktop.updater.dialog.upToDate.message": "最新の状態です。", - "desktop.updater.dialog.upToDate.title": "アップデートはありません", - "desktop.updater.dialog.ready.message": "アップデート{{version}}をダウンロードしました。今すぐ再起動しますか?", - "desktop.updater.dialog.ready.title": "アップデートの準備ができました", - "desktop.updater.dialog.restart": "再起動", - "desktop.updater.dialog.later": "後で", - "desktop.recovery.action.relaunch": "再起動", - "desktop.recovery.action.exportLogs": "ログをエクスポート", - "desktop.recovery.action.keepWaiting": "待機を続ける", - "desktop.recovery.action.quit": "終了", - "desktop.recovery.loadFailed": "OpenCodeを読み込めませんでした", - "desktop.recovery.terminated": "OpenCodeのウインドウが予期せず終了しました", - "desktop.recovery.unresponsive": "OpenCodeが応答していません", - "desktop.recovery.unresponsive.detail": "アプリを再起動するか、ログを開くか、待機を続けることができます。", - "desktop.recovery.loadFailed.detail": "ウインドウ: {{window}}\nURL: {{url}}\nエラー: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "ウインドウ: {{window}}\n理由: {{reason}}\nコード: {{code}}", - "desktop.recovery.unknown": "<不明>", - "desktop.dialog.chooseFolder": "フォルダを選択", - "desktop.dialog.chooseFile": "ファイルを選択", - "desktop.dialog.saveFile": "ファイルを保存", - "desktop.dialog.files": "ファイル", - "desktop.server.local": "ローカルサーバー", - "desktop.wsl.error.windowsOnly": "WSLはWindowsでのみ利用できます", - "desktop.wsl.error.unavailable": "WSLを利用できません", - "desktop.wsl.error.listInstalled": "インストール済みのWSLディストリビューションの一覧を取得できませんでした", - "desktop.wsl.error.listOnline": "オンラインのWSLディストリビューションの一覧を取得できませんでした", - "desktop.wsl.error.executeDistro": "ディストリビューションでコマンドを実行できません", - "desktop.wsl.error.installWsl": "WSLのインストールに失敗しました", - "desktop.wsl.error.installDistro": "ディストリビューションのインストールに失敗しました: {{distro}}", - "desktop.wsl.error.installOpencode": "OpenCodeのインストールに失敗しました", - "desktop.wsl.error.alreadyAdded": "{{distro}}はすでに追加されています", - "desktop.wsl.error.opencodeMissing": "このディストリビューションにはopencodeがインストールされていません", - "desktop.wsl.error.opencodeCannotRun": "opencodeはインストールされていますが、実行できませんでした", - "desktop.wsl.error.opencodeNotInstalled": "{{distro}}にはOpenCodeがインストールされていません", - "desktop.wsl.error.updateVersion": "OpenCodeのアップデートは完了しましたが、{{distro}}から報告されたバージョンはまだ{{installed}}です。想定されるバージョンは{{expected}}です", - "desktop.wsl.error.noVersion": "バージョンなし", - "desktop.wsl.error.serverExited": "WSLサーバーが起動後に終了しました (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSLサーバーが正常に稼働する前に終了しました (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}}のSidecarのヘルスチェックが{{timeout}}ms後にタイムアウトしました", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}}が{{timeout}}ms後にタイムアウトしました", - "desktop.wsl.error.failedPort": "ポートを取得できませんでした", - "desktop.picker.error.notSelected": "ピッカーでファイルが選択されませんでした", - "desktop.picker.error.sizeLimit": "選択した添付ファイルが上限の{{limit}} MBを超えています", - "command.logs.export": "ログをエクスポート", - "command.session.export": "セッションをエクスポート", - "command.session.export.description": "セッションの全記録を JSON としてエクスポート", - "provider.connect.method.browser": "ブラウザ", - "provider.connect.method.headless": "ヘッドレス モード", - "provider.custom.unavailable": "このサーバーではカスタムプロバイダーを利用できません", - "help.tabs.toast.ariaLabel": "タブのご紹介。タブを使って作業と進行中のセッションを整理できます", - "help.tabs.toast.dismiss": "タブに関する案内を閉じる", - "help.tabs.title": "タブのご紹介", - "help.tabs.description": "タブを使って作業と進行中のセッションを整理できます", - "help.tabs.date": "7月14日", - "help.tabs.introduction": "OpenCode Desktopでタブを中心とした操作ができるようになりました。", - "help.tabs.sessions": "新しいセッションをタブで開始したり、プロジェクトから既存のセッションを開いたりできます。新しい作業を始めるときは新しいタブを開き、作業が終わったら閉じます。", - "help.tabs.organize": "複数のタブを開いておくと、進行中のセッションを整理しやすくなります。タブを残しておく場合は、覚えやすい名前に変更できます。", - "help.tabs.home": "新しいホーム画面には、すべてのセッションとプロジェクトが表示されます。セッションを選択するとタブで開きます。", - "help.tabs.persistence": "アプリを再度開いたときも、タブは開いたままです。", - "help.tabs.worktrees": "新しいデザインはまだGitワークツリーに対応していませんが、近日中に対応する予定です。以前のレイアウトを引き続き使用する場合は、設定でレイアウトを切り替えることができます。新しいレイアウトは数週間後に標準となる予定です。", - "dialog.usageExceeded.dontShowAgain": "今後表示しない", - "context.export.session": "セッションをエクスポート", - "toast.session.export.success.title": "セッションをエクスポートしました", - "toast.session.export.success.description": "セッションを {{filename}} に保存しました", - "toast.session.export.failed.title": "セッションのエクスポートに失敗しました", - "toast.session.export.failed.description": "セッションのエクスポート中にエラーが発生しました", - "common.export": "エクスポート", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "すべてのインタラクティブ要素にフォーカススタイルを強制適用", - "debugBar.focus.on": "オン", - "debugBar.focus.off": "オフ", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "選択中の言語を変更せずに、アプリ全体のレイアウト方向を強制設定", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "terminal.connectTicket.csrfError": "PTY接続チケットは、オリジンまたはCSRFチェックによって拒否されました。サーバーのCORS設定を確認してください。", - "terminal.connectTicket.statusError": "PTY接続チケットがステータス {{status}} で失敗しました", - "titlebar.update": "アップデート", - "titlebar.updateVersion": "アップデート {{version}}", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts index 759865c7..3c871057 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts @@ -1141,135 +1141,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "command.session.export": "세션 내보내기", - "command.session.export.description": "전체 세션 기록을 JSON으로 내보내기", - "provider.connect.method.browser": "브라우저", - "provider.connect.method.headless": "헤드리스 모드", - "provider.custom.unavailable": "이 서버에서는 사용자 지정 공급자를 사용할 수 없습니다", - "dialog.usageExceeded.dontShowAgain": "다시 표시하지 않기", - "context.export.session": "세션 내보내기", - "toast.session.export.success.title": "세션을 내보냈습니다", - "toast.session.export.success.description": "세션 저장 위치: {{filename}}", - "toast.session.export.failed.title": "세션 내보내기 실패", - "toast.session.export.failed.description": "세션을 내보내는 동안 오류가 발생했습니다", - "common.export": "내보내기", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "모든 대화형 요소에 포커스 스타일 강제 적용", - "debugBar.focus.on": "켬", - "debugBar.focus.off": "끔", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "선택한 언어를 변경하지 않고 전체 앱의 레이아웃 방향 강제 설정", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "terminal.connectTicket.csrfError": "PTY 연결 티켓이 오리진 또는 CSRF 검사에서 거부되었습니다. 서버의 CORS 구성을 확인하세요.", - "terminal.connectTicket.statusError": "PTY 연결 티켓 실패(상태: {{status}})", - "titlebar.update": "업데이트", - "titlebar.updateVersion": "업데이트 {{version}}", - "command.logs.export": "로그 내보내기", - "help.tabs.toast.ariaLabel": "탭을 소개합니다. 탭으로 작업과 활성 세션을 정리하세요", - "help.tabs.toast.dismiss": "탭 정보 닫기", - "help.tabs.title": "탭 소개", - "help.tabs.description": "탭으로 작업과 활성 세션을 정리하세요", - "help.tabs.date": "7월 14일", - "help.tabs.introduction": "이제 OpenCode Desktop은 탭을 중심으로 구성됩니다.", - "help.tabs.sessions": "탭에서 새 세션을 시작하거나 프로젝트에서 기존 세션을 여세요. 새로운 작업을 시작할 때 새 탭을 열고, 작업을 마치면 닫으세요.", - "help.tabs.organize": "탭을 몇 개 열어 두면 활성 세션을 더 쉽게 정리할 수 있습니다. 탭을 계속 열어 둘 예정이라면 기억하기 쉬운 이름으로 바꾸세요.", - "help.tabs.home": "새 홈 화면에서 모든 세션과 프로젝트를 확인할 수 있습니다. 세션을 선택하면 탭에서 열립니다.", - "help.tabs.persistence": "앱을 다시 열어도 탭은 그대로 열려 있습니다.", - "help.tabs.worktrees": "새 디자인은 아직 Git Worktrees를 지원하지 않지만 곧 지원할 예정입니다. 이전 레이아웃을 계속 사용하려면 설정에서 레이아웃을 전환할 수 있습니다. 단, 몇 주 후에는 새 레이아웃이 영구 적용됩니다.", - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "파일", - "desktop.menu.edit": "편집", - "desktop.menu.view": "보기", - "desktop.menu.go": "이동", - "desktop.menu.window": "윈도우", - "desktop.menu.help": "도움말", - "desktop.menu.checkForUpdates": "업데이트 확인...", - "desktop.menu.settings": "설정", - "desktop.menu.reloadWebview": "WebView 새로 고침", - "desktop.menu.restart": "다시 시작", - "desktop.menu.exportLogs": "로그 내보내기...", - "desktop.menu.newSession": "새 세션", - "desktop.menu.openProject": "프로젝트 열기...", - "desktop.menu.newWindow": "새 윈도우", - "desktop.menu.closeWindow": "윈도우 닫기", - "desktop.menu.undo": "실행 취소", - "desktop.menu.redo": "다시 실행", - "desktop.menu.cut": "잘라내기", - "desktop.menu.copy": "복사", - "desktop.menu.paste": "붙여넣기", - "desktop.menu.delete": "삭제", - "desktop.menu.selectAll": "모두 선택", - "desktop.menu.toggleSidebar": "사이드바 표시/숨기기", - "desktop.menu.toggleTerminal": "터미널 표시/숨기기", - "desktop.menu.toggleFileTree": "파일 트리 표시/숨기기", - "desktop.menu.reload": "새로 고침", - "desktop.menu.toggleDeveloperTools": "개발자 도구 표시/숨기기", - "desktop.menu.actualSize": "실제 크기", - "desktop.menu.zoomIn": "확대", - "desktop.menu.zoomOut": "축소", - "desktop.menu.toggleFullScreen": "전체 화면 전환", - "desktop.menu.back": "뒤로", - "desktop.menu.forward": "앞으로", - "desktop.menu.previousSession": "이전 세션", - "desktop.menu.nextSession": "다음 세션", - "desktop.menu.previousProject": "이전 프로젝트", - "desktop.menu.nextProject": "다음 프로젝트", - "desktop.menu.minimize": "최소화", - "desktop.menu.maximize": "최대화", - "desktop.menu.documentation": "OpenCode 문서", - "desktop.menu.supportForum": "지원 포럼", - "desktop.menu.shareFeedback": "피드백 보내기", - "desktop.menu.reportBug": "버그 신고", - "desktop.menu.ariaLabel": "OpenCode 메뉴", - "desktop.updater.dialog.checkFailed.message": "업데이트를 확인하지 못했습니다.", - "desktop.updater.dialog.checkFailed.title": "업데이트 오류", - "desktop.updater.dialog.upToDate.message": "최신 버전을 사용 중입니다.", - "desktop.updater.dialog.upToDate.title": "업데이트 없음", - "desktop.updater.dialog.ready.message": "업데이트 {{version}} 다운로드가 완료되었습니다. 지금 다시 시작할까요?", - "desktop.updater.dialog.ready.title": "업데이트 준비 완료", - "desktop.updater.dialog.restart": "다시 시작", - "desktop.updater.dialog.later": "나중에", - "desktop.recovery.action.relaunch": "다시 실행", - "desktop.recovery.action.exportLogs": "로그 내보내기", - "desktop.recovery.action.keepWaiting": "계속 기다리기", - "desktop.recovery.action.quit": "종료", - "desktop.recovery.loadFailed": "OpenCode를 불러오지 못했습니다", - "desktop.recovery.terminated": "OpenCode 윈도우가 예기치 않게 종료되었습니다", - "desktop.recovery.unresponsive": "OpenCode가 응답하지 않습니다", - "desktop.recovery.unresponsive.detail": "앱을 다시 실행하거나 로그를 열거나 계속 기다릴 수 있습니다.", - "desktop.recovery.loadFailed.detail": "윈도우: {{window}}\nURL: {{url}}\n오류: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "윈도우: {{window}}\n원인: {{reason}}\n코드: {{code}}", - "desktop.recovery.unknown": "<알 수 없음>", - "desktop.dialog.chooseFolder": "폴더 선택", - "desktop.dialog.chooseFile": "파일 선택", - "desktop.dialog.saveFile": "파일 저장", - "desktop.dialog.files": "파일", - "desktop.server.local": "로컬 서버", - "desktop.wsl.error.windowsOnly": "WSL은 Windows에서만 사용할 수 있습니다", - "desktop.wsl.error.unavailable": "WSL을 사용할 수 없습니다", - "desktop.wsl.error.listInstalled": "설치된 WSL 배포판 목록을 가져오지 못했습니다", - "desktop.wsl.error.listOnline": "온라인 WSL 배포판 목록을 가져오지 못했습니다", - "desktop.wsl.error.executeDistro": "배포판에서 명령을 실행할 수 없습니다", - "desktop.wsl.error.installWsl": "WSL 설치 실패", - "desktop.wsl.error.installDistro": "배포판 설치 실패: {{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode 설치 실패", - "desktop.wsl.error.alreadyAdded": "{{distro}} 배포판은 이미 추가되었습니다", - "desktop.wsl.error.opencodeMissing": "이 배포판에 opencode가 설치되어 있지 않습니다", - "desktop.wsl.error.opencodeCannotRun": "opencode가 설치되어 있지만 실행할 수 없습니다", - "desktop.wsl.error.opencodeNotInstalled": "{{distro}}에 OpenCode가 설치되어 있지 않습니다", - "desktop.wsl.error.updateVersion": "OpenCode 업데이트가 완료되었지만 {{distro}}에서 여전히 {{installed}} 버전으로 표시됩니다. 예상 버전: {{expected}}", - "desktop.wsl.error.noVersion": "버전 없음", - "desktop.wsl.error.serverExited": "WSL 서버가 시작 후 종료되었습니다(코드={{code}} 신호={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL 서버가 정상 상태가 되기 전에 종료되었습니다(코드={{code}} 신호={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}} 사이드카의 상태 확인이 {{timeout}}ms 후 시간 초과되었습니다", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 명령이 {{timeout}}ms 후 시간 초과되었습니다", - "desktop.wsl.error.failedPort": "포트를 가져오지 못했습니다", - "desktop.picker.error.notSelected": "선택기에서 파일을 선택하지 않았습니다", - "desktop.picker.error.sizeLimit": "선택한 첨부 파일이 {{limit}} MB 제한을 초과합니다", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts index 58ead9d2..21fa4438 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts @@ -1237,135 +1237,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Arkiv", - "desktop.menu.edit": "Rediger", - "desktop.menu.view": "Vis", - "desktop.menu.go": "Gå", - "desktop.menu.window": "Vindu", - "desktop.menu.help": "Hjelp", - "desktop.menu.checkForUpdates": "Søk etter oppdateringer...", - "desktop.menu.settings": "Innstillinger", - "desktop.menu.reloadWebview": "Last inn WebView på nytt", - "desktop.menu.restart": "Start på nytt", - "desktop.menu.exportLogs": "Eksporter logger...", - "desktop.menu.newSession": "Ny sesjon", - "desktop.menu.openProject": "Åpne prosjekt...", - "desktop.menu.newWindow": "Nytt vindu", - "desktop.menu.closeWindow": "Lukk vindu", - "desktop.menu.undo": "Angre", - "desktop.menu.redo": "Gjør om", - "desktop.menu.cut": "Klipp ut", - "desktop.menu.copy": "Kopier", - "desktop.menu.paste": "Lim inn", - "desktop.menu.delete": "Slett", - "desktop.menu.selectAll": "Merk alt", - "desktop.menu.toggleSidebar": "Vis eller skjul sidepanel", - "desktop.menu.toggleTerminal": "Vis eller skjul terminal", - "desktop.menu.toggleFileTree": "Vis eller skjul filtreet", - "desktop.menu.reload": "Last inn på nytt", - "desktop.menu.toggleDeveloperTools": "Vis eller skjul utviklerverktøy", - "desktop.menu.actualSize": "Faktisk størrelse", - "desktop.menu.zoomIn": "Zoom inn", - "desktop.menu.zoomOut": "Zoom ut", - "desktop.menu.toggleFullScreen": "Slå fullskjerm av eller på", - "desktop.menu.back": "Tilbake", - "desktop.menu.forward": "Frem", - "desktop.menu.previousSession": "Forrige sesjon", - "desktop.menu.nextSession": "Neste sesjon", - "desktop.menu.previousProject": "Forrige prosjekt", - "desktop.menu.nextProject": "Neste prosjekt", - "desktop.menu.minimize": "Minimer", - "desktop.menu.maximize": "Maksimer", - "desktop.menu.documentation": "OpenCode-dokumentasjon", - "desktop.menu.supportForum": "Brukerstøtteforum", - "desktop.menu.shareFeedback": "Del tilbakemelding", - "desktop.menu.reportBug": "Rapporter en feil", - "desktop.menu.ariaLabel": "OpenCode-meny", - "desktop.updater.dialog.checkFailed.message": "Søket etter oppdateringer mislyktes.", - "desktop.updater.dialog.checkFailed.title": "Oppdateringsfeil", - "desktop.updater.dialog.upToDate.message": "Du har den nyeste versjonen.", - "desktop.updater.dialog.upToDate.title": "Ingen oppdateringer", - "desktop.updater.dialog.ready.message": "Oppdateringen {{version}} er lastet ned. Vil du starte på nytt nå?", - "desktop.updater.dialog.ready.title": "Oppdateringen er klar", - "desktop.updater.dialog.restart": "Start på nytt", - "desktop.updater.dialog.later": "Senere", - "desktop.recovery.action.relaunch": "Start appen på nytt", - "desktop.recovery.action.exportLogs": "Eksporter logger", - "desktop.recovery.action.keepWaiting": "Fortsett å vente", - "desktop.recovery.action.quit": "Avslutt", - "desktop.recovery.loadFailed": "OpenCode kunne ikke lastes inn", - "desktop.recovery.terminated": "OpenCode-vinduet ble uventet avsluttet", - "desktop.recovery.unresponsive": "OpenCode svarer ikke", - "desktop.recovery.unresponsive.detail": "Du kan starte appen på nytt, åpne loggene eller fortsette å vente.", - "desktop.recovery.loadFailed.detail": "Vindu: {{window}}\nURL: {{url}}\nFeil: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Vindu: {{window}}\nÅrsak: {{reason}}\nKode: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Velg en mappe", - "desktop.dialog.chooseFile": "Velg en fil", - "desktop.dialog.saveFile": "Lagre fil", - "desktop.dialog.files": "Filer", - "desktop.server.local": "Lokal server", - "desktop.wsl.error.windowsOnly": "WSL er bare tilgjengelig på Windows", - "desktop.wsl.error.unavailable": "WSL er utilgjengelig", - "desktop.wsl.error.listInstalled": "Kunne ikke vise installerte WSL-distribusjoner", - "desktop.wsl.error.listOnline": "Kunne ikke vise tilgjengelige WSL-distribusjoner", - "desktop.wsl.error.executeDistro": "Kan ikke kjøre kommandoer i distribusjonen", - "desktop.wsl.error.installWsl": "Installasjonen av WSL mislyktes", - "desktop.wsl.error.installDistro": "Kunne ikke installere distribusjonen: {{distro}}", - "desktop.wsl.error.installOpencode": "Installasjonen av OpenCode mislyktes", - "desktop.wsl.error.alreadyAdded": "{{distro}} er allerede lagt til", - "desktop.wsl.error.opencodeMissing": "opencode er ikke installert i denne distribusjonen", - "desktop.wsl.error.opencodeCannotRun": "opencode er installert, men kunne ikke kjøres", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode er ikke installert i {{distro}}", - "desktop.wsl.error.updateVersion": "Oppdateringen av OpenCode er fullført, men {{distro}} rapporterer fortsatt {{installed}}; forventet {{expected}}", - "desktop.wsl.error.noVersion": "ingen versjon", - "desktop.wsl.error.serverExited": "WSL-serveren ble avsluttet etter oppstart (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL-serveren ble avsluttet før den var klar (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Tidsavbrudd for tilstandskontrollen av sidecar for {{distro}} etter {{timeout}} ms", - "desktop.wsl.error.commandTimeout": "Tidsavbrudd for {{command}} {{args}} etter {{timeout}} ms", - "desktop.wsl.error.failedPort": "Kunne ikke hente port", - "desktop.picker.error.notSelected": "Ingen fil ble valgt i filvelgeren", - "desktop.picker.error.sizeLimit": "De valgte vedleggene overskrider grensen på {{limit}} MB", - "command.logs.export": "Eksporter logger", - "command.session.export": "Eksporter sesjon", - "command.session.export.description": "Eksporter hele sesjonsutskriften som JSON", - "provider.connect.method.browser": "Nettleser", - "provider.connect.method.headless": "Uten nettleser", - "provider.custom.unavailable": "Egendefinerte leverandører er ikke tilgjengelige på denne serveren", - "dialog.usageExceeded.dontShowAgain": "Ikke vis igjen", - "context.export.session": "Eksporter sesjon", - "toast.session.export.success.title": "Sesjon eksportert", - "toast.session.export.success.description": "Sesjonen ble lagret i {{filename}}", - "toast.session.export.failed.title": "Kunne ikke eksportere sesjon", - "toast.session.export.failed.description": "Det oppstod en feil under eksport av sesjonen", - "terminal.connectTicket.csrfError": "Tilkoblingsbilletten for PTY ble avvist av opprinnelses- eller CSRF-kontroller. Kontroller serverens CORS-konfigurasjon.", - "terminal.connectTicket.statusError": "Tilkoblingsbilletten for PTY mislyktes med {{status}}", - "titlebar.update": "Oppdater", - "titlebar.updateVersion": "Oppdater {{version}}", - "common.export": "Eksporter", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Tving fokusstiler på alle interaktive elementer", - "debugBar.focus.on": "PÅ", - "debugBar.focus.off": "AV", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Tving layoutretningen for hele appen uten å endre valgt språk", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "help.tabs.toast.ariaLabel": "Vi introduserer faner. Organiser arbeidet og de aktive sesjonene dine med faner", - "help.tabs.toast.dismiss": "Avvis informasjon om faner", - "help.tabs.title": "Vi introduserer faner", - "help.tabs.description": "Organiser arbeidet og de aktive sesjonene dine med faner", - "help.tabs.date": "14. juli", - "help.tabs.introduction": "OpenCode Desktop er nå bygget rundt faner.", - "help.tabs.sessions": "Start en ny sesjon i en fane, eller åpne en eksisterende sesjon fra et av prosjektene dine. Åpne en ny fane når du skal begynne på noe nytt, og lukk den når du er ferdig.", - "help.tabs.organize": "Når du har noen få faner åpne, blir det enklere å organisere de aktive sesjonene. Gi fanene navn som er lette å huske, hvis du planlegger å beholde dem.", - "help.tabs.home": "Du finner alle sesjonene og prosjektene dine på den nye Hjem-skjermen. Når du velger en sesjon, åpnes den i en fane.", - "help.tabs.persistence": "Fanene er fortsatt åpne når du åpner appen på nytt.", - "help.tabs.worktrees": "Det nye oppsettet støtter ikke Git Worktrees ennå, men støtte kommer snart. Hvis du foretrekker å fortsette med det forrige oppsettet, kan du bytte mellom oppsettene i Innstillinger. Vær oppmerksom på at det nye oppsettet blir permanent om noen uker.", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/parity.test.ts b/packages/app-bundle/overlay/packages/app/src/i18n/parity.test.ts index 2b5e8e23..566be37e 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/parity.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/parity.test.ts @@ -1,32 +1,6 @@ -// Overlaid at the v1.18.29 re-base (#796): upstream's parity test scoped to the -// overlay's dict set — the app/ui domains check the 18 overlay locales (Amicode -// keys are not translated into upstream's 28 extra locales); the desktop domain -// is pure upstream and checks the full set. import { describe, expect, test } from "bun:test" -import { desktopNativePluralCategories } from "./desktop-native" -// Overlay scope (#796): the app/ui dicts the overlay carries — the Amicode-branded -// keys exist only in these, so parity is checked against this set, not upstream's 46. const appLocales = [ - "ar", - "br", - "bs", - "da", - "de", - "es", - "fr", - "ja", - "ko", - "no", - "pl", - "ru", - "th", - "tr", - "uk", - "zh", - "zht", -] as const -const desktopLocales = [ "ar", "br", "bs", @@ -44,60 +18,8 @@ const desktopLocales = [ "tr", "zh", "zht", - "hi", - "nl", - "id", - "vi", - "it", - "ur", - "pa", - "az", - "fi", - "sv", - "am", - "bg", - "bn", - "ca", - "cs", - "dv", - "dz", - "el", - "et", - "fa", - "fo", - "hr", - "hu", - "hy", - "is", - "ka", - "km", - "lo", - "lt", - "lv", - "mk", - "mn", - "ms", - "my", - "ne", - "ro", - "si", - "sk", - "sl", - "sq", - "sr", - "tg", - "tk", - "uz", ] as const -const pluralCategories = new Map( - appLocales.map( - (locale) => - [ - locale, - desktopNativePluralCategories(locale).filter((category) => category !== "one" && category !== "other"), - ] as const, - ), -) +const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr") const domains = [ { @@ -120,24 +42,19 @@ const domains = [ }, ] as const -describe("i18n parity", () => { - test("non-English locales have every English key and required plural variants", async () => { +describe.skipIf(!!process.env.CI)("i18n parity", () => { + test("non-English locales have every English key", async () => { for (const domain of domains) { const source = await dictionary(domain.source) for (const locale of domain.locales) { const target = await dictionary(domain.target(locale)) const missing = Object.keys(source).filter((key) => !Object.hasOwn(target, key)) - const extra = Object.keys(target) - .filter((key) => !Object.hasOwn(source, key)) - .sort() - const expected = pluralFamilies(source) - .flatMap((key) => (pluralCategories.get(locale) ?? []).map((category) => `${key}.${category}`)) - .sort() + const extra = Object.keys(target).filter((key) => !Object.hasOwn(source, key)) expect({ domain: domain.name, locale, missing, extra }).toEqual({ domain: domain.name, locale, missing: [], - extra: expected, + extra: [], }) } } @@ -151,17 +68,7 @@ describe("i18n parity", () => { const mismatched = Object.keys(source).filter( (key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(), ) - const pluralMismatched = pluralFamilies(source).flatMap((key) => - (pluralCategories.get(locale) ?? []) - .map((category) => `${key}.${category}`) - .filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()), - ) - expect({ domain: domain.name, locale, mismatched, pluralMismatched }).toEqual({ - domain: domain.name, - locale, - mismatched: [], - pluralMismatched: [], - }) + expect({ domain: domain.name, locale, mismatched }).toEqual({ domain: domain.name, locale, mismatched: [] }) } } }) @@ -193,38 +100,6 @@ describe("i18n parity", () => { }) }) -describe("i18n plural parity", () => { - test("locale-specific categories exist and preserve count placeholders", async () => { - for (const domain of domains.slice(0, 2)) { - const source = await dictionary(domain.source) - const families = pluralFamilies(source) - for (const locale of domain.locales) { - const target = await dictionary(domain.target(locale)) - const missing = families.flatMap((key) => - (pluralCategories.get(locale) ?? []) - .map((category) => `${key}.${category}`) - .filter((variant) => !Object.hasOwn(target, variant)), - ) - const mismatched = families.flatMap((key) => - (pluralCategories.get(locale) ?? []) - .map((category) => `${key}.${category}`) - .filter( - (variant) => - Object.hasOwn(target, variant) && - placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join(), - ), - ) - expect({ domain: domain.name, locale, missing, mismatched }).toEqual({ - domain: domain.name, - locale, - missing: [], - mismatched: [], - }) - } - } - }) -}) - async function dictionary(file: string) { const module: unknown = await import(file) if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) { @@ -241,14 +116,3 @@ function isDictionary(value: unknown): value is Record { function placeholders(value: string) { return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1]).sort() } - -function pluralFamilies(dictionary: Record) { - return Object.keys(dictionary) - .filter( - (key) => - key.endsWith(".one") && - dictionary[key].includes("{{count}}") && - dictionary[`${key.slice(0, -4)}.other`]?.includes("{{count}}"), - ) - .map((key) => key.slice(0, -4)) -} diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts index a3446bff..8bee1740 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts @@ -1150,141 +1150,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Plik", - "desktop.menu.edit": "Edycja", - "desktop.menu.view": "Widok", - "desktop.menu.go": "Przejdź", - "desktop.menu.window": "Okno", - "desktop.menu.help": "Pomoc", - "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...", - "desktop.menu.settings": "Ustawienia", - "desktop.menu.reloadWebview": "Wczytaj ponownie widok internetowy", - "desktop.menu.restart": "Uruchom ponownie", - "desktop.menu.exportLogs": "Eksportuj dzienniki...", - "desktop.menu.newSession": "Nowa sesja", - "desktop.menu.openProject": "Otwórz projekt...", - "desktop.menu.newWindow": "Nowe okno", - "desktop.menu.closeWindow": "Zamknij okno", - "desktop.menu.undo": "Cofnij", - "desktop.menu.redo": "Ponów", - "desktop.menu.cut": "Wytnij", - "desktop.menu.copy": "Kopiuj", - "desktop.menu.paste": "Wklej", - "desktop.menu.delete": "Usuń", - "desktop.menu.selectAll": "Zaznacz wszystko", - "desktop.menu.toggleSidebar": "Przełącz pasek boczny", - "desktop.menu.toggleTerminal": "Przełącz terminal", - "desktop.menu.toggleFileTree": "Przełącz drzewo plików", - "desktop.menu.reload": "Wczytaj ponownie", - "desktop.menu.toggleDeveloperTools": "Przełącz narzędzia deweloperskie", - "desktop.menu.actualSize": "Rozmiar rzeczywisty", - "desktop.menu.zoomIn": "Powiększ", - "desktop.menu.zoomOut": "Pomniejsz", - "desktop.menu.toggleFullScreen": "Przełącz tryb pełnoekranowy", - "desktop.menu.back": "Wstecz", - "desktop.menu.forward": "Dalej", - "desktop.menu.previousSession": "Poprzednia sesja", - "desktop.menu.nextSession": "Następna sesja", - "desktop.menu.previousProject": "Poprzedni projekt", - "desktop.menu.nextProject": "Następny projekt", - "desktop.menu.minimize": "Minimalizuj", - "desktop.menu.maximize": "Maksymalizuj", - "desktop.menu.documentation": "Dokumentacja OpenCode", - "desktop.menu.supportForum": "Forum pomocy technicznej", - "desktop.menu.shareFeedback": "Prześlij opinię", - "desktop.menu.reportBug": "Zgłoś błąd", - "desktop.menu.ariaLabel": "Menu OpenCode", - "desktop.updater.dialog.checkFailed.message": "Nie udało się sprawdzić dostępności aktualizacji.", - "desktop.updater.dialog.checkFailed.title": "Błąd aktualizacji", - "desktop.updater.dialog.upToDate.message": "Masz najnowszą wersję.", - "desktop.updater.dialog.upToDate.title": "Brak aktualizacji", - "desktop.updater.dialog.ready.message": "Pobrano aktualizację {{version}}. Uruchomić ponownie teraz?", - "desktop.updater.dialog.ready.title": "Aktualizacja jest gotowa", - "desktop.updater.dialog.restart": "Uruchom ponownie", - "desktop.updater.dialog.later": "Później", - "desktop.recovery.action.relaunch": "Uruchom ponownie", - "desktop.recovery.action.exportLogs": "Eksportuj dzienniki", - "desktop.recovery.action.keepWaiting": "Czekaj dalej", - "desktop.recovery.action.quit": "Zakończ", - "desktop.recovery.loadFailed": "Nie udało się wczytać OpenCode", - "desktop.recovery.terminated": "Okno OpenCode zostało nieoczekiwanie zamknięte", - "desktop.recovery.unresponsive": "OpenCode nie odpowiada", - "desktop.recovery.unresponsive.detail": "Możesz ponownie uruchomić aplikację, otworzyć dzienniki lub nadal czekać.", - "desktop.recovery.loadFailed.detail": "Okno: {{window}}\nURL: {{url}}\nBłąd: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Okno: {{window}}\nPrzyczyna: {{reason}}\nKod: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Wybierz folder", - "desktop.dialog.chooseFile": "Wybierz plik", - "desktop.dialog.saveFile": "Zapisz plik", - "desktop.dialog.files": "Pliki", - "desktop.server.local": "Serwer lokalny", - "desktop.wsl.error.windowsOnly": "WSL jest dostępny tylko w systemie Windows", - "desktop.wsl.error.unavailable": "WSL jest niedostępny", - "desktop.wsl.error.listInstalled": "Nie udało się wyświetlić listy zainstalowanych dystrybucji WSL", - "desktop.wsl.error.listOnline": "Nie udało się wyświetlić listy dystrybucji WSL dostępnych online", - "desktop.wsl.error.executeDistro": "Nie można wykonywać poleceń w dystrybucji", - "desktop.wsl.error.installWsl": "Nie udało się zainstalować WSL", - "desktop.wsl.error.installDistro": "Nie udało się zainstalować dystrybucji: {{distro}}", - "desktop.wsl.error.installOpencode": "Nie udało się zainstalować OpenCode", - "desktop.wsl.error.alreadyAdded": "Dystrybucja {{distro}} została już dodana", - "desktop.wsl.error.opencodeMissing": "Program opencode nie jest zainstalowany w tej dystrybucji", - "desktop.wsl.error.opencodeCannotRun": "Program opencode jest zainstalowany, ale nie można go uruchomić", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode nie jest zainstalowany w dystrybucji {{distro}}", - "desktop.wsl.error.updateVersion": "Aktualizacja OpenCode została ukończona, ale dystrybucja {{distro}} nadal zgłasza wersję {{installed}}; oczekiwano {{expected}}", - "desktop.wsl.error.noVersion": "brak wersji", - "desktop.wsl.error.serverExited": "Serwer WSL zakończył działanie po uruchomieniu (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "Serwer WSL zakończył działanie przed osiągnięciem gotowości (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Przekroczono limit czasu {{timeout}}ms podczas sprawdzania kondycji procesu sidecar dla dystrybucji {{distro}}", - "desktop.wsl.error.commandTimeout": "Polecenie {{command}} {{args}} przekroczyło limit czasu {{timeout}}ms", - "desktop.wsl.error.failedPort": "Nie udało się uzyskać portu", - "desktop.picker.error.notSelected": "Nie wybrano pliku w oknie wyboru", - "desktop.picker.error.sizeLimit": "Wybrane załączniki przekraczają limit {{limit}} MB", - "command.logs.export": "Eksportuj dzienniki", - "command.session.export": "Eksportuj sesję", - "command.session.export.description": "Eksportuj pełny zapis sesji w formacie JSON", - "provider.connect.method.browser": "Przeglądarka", - "provider.connect.method.headless": "Bez przeglądarki", - "provider.custom.unavailable": "Niestandardowi dostawcy są niedostępni na tym serwerze", - "help.tabs.toast.ariaLabel": "Przedstawiamy karty. Organizuj pracę i aktywne sesje za pomocą kart", - "help.tabs.toast.dismiss": "Odrzuć informacje o kartach", - "help.tabs.title": "Przedstawiamy karty", - "help.tabs.description": "Organizuj pracę i aktywne sesje za pomocą kart", - "help.tabs.date": "14 lipca", - "help.tabs.introduction": "OpenCode Desktop korzysta teraz z układu opartego na kartach.", - "help.tabs.sessions": "Rozpocznij nową sesję na karcie lub otwórz istniejącą sesję z dowolnego projektu. Otwórz nową kartę, gdy zaczynasz coś nowego, i zamknij ją po zakończeniu.", - "help.tabs.organize": "Kilka otwartych kart ułatwia organizowanie aktywnych sesji. Jeśli chcesz zachować karty na dłużej, nadaj im łatwe do zapamiętania nazwy.", - "help.tabs.home": "Wszystkie sesje i projekty znajdziesz na nowym ekranie głównym. Wybranie sesji powoduje otwarcie jej na karcie.", - "help.tabs.persistence": "Po ponownym otwarciu aplikacji karty pozostają otwarte.", - "help.tabs.worktrees": "Nowy układ nie obsługuje jeszcze drzew roboczych Git, ale ta funkcja pojawi się wkrótce. Jeśli wolisz nadal korzystać z poprzedniego układu, możesz przełączać układy w Ustawieniach. Pamiętaj jednak, że za kilka tygodni nowy układ stanie się jedynym dostępnym.", - "dialog.usageExceeded.dontShowAgain": "Nie pokazuj ponownie", - "context.export.session": "Eksportuj sesję", - "toast.session.export.success.title": "Sesja wyeksportowana", - "toast.session.export.success.description": "Sesję zapisano jako {{filename}}", - "toast.session.export.failed.title": "Nie udało się wyeksportować sesji", - "toast.session.export.failed.description": "Wystąpił błąd podczas eksportowania sesji", - "session.question.pending.few": "{{count}} oczekujące pytania", - "session.question.pending.many": "{{count}} oczekujących pytań", - "session.followupDock.summary.few": "{{count}} wiadomości w kolejce", - "session.followupDock.summary.many": "{{count}} wiadomości w kolejce", - "session.revertDock.summary.few": "{{count}} cofnięte wiadomości", - "session.revertDock.summary.many": "{{count}} cofniętych wiadomości", - "terminal.connectTicket.csrfError": "Bilet połączenia PTY został odrzucony podczas kontroli źródła lub CSRF. Sprawdź konfigurację CORS serwera.", - "terminal.connectTicket.statusError": "Bilet połączenia PTY zakończył się niepowodzeniem ze statusem {{status}}", - "titlebar.update": "Aktualizuj", - "titlebar.updateVersion": "Aktualizuj {{version}}", - "common.export": "Eksportuj", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Wymuś style fokusu dla wszystkich elementów interaktywnych", - "debugBar.focus.on": "WŁ.", - "debugBar.focus.off": "WYŁ.", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Wymuś kierunek układu całej aplikacji bez zmiany wybranego języka", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts index ea3bbbe0..19de4d5d 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts @@ -1229,141 +1229,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Файл", - "desktop.menu.edit": "Правка", - "desktop.menu.view": "Вид", - "desktop.menu.go": "Переход", - "desktop.menu.window": "Окно", - "desktop.menu.help": "Справка", - "desktop.menu.checkForUpdates": "Проверить наличие обновлений...", - "desktop.menu.settings": "Настройки", - "desktop.menu.reloadWebview": "Перезагрузить веб-представление", - "desktop.menu.restart": "Перезапустить", - "desktop.menu.exportLogs": "Экспортировать журналы...", - "desktop.menu.newSession": "Новая сессия", - "desktop.menu.openProject": "Открыть проект...", - "desktop.menu.newWindow": "Новое окно", - "desktop.menu.closeWindow": "Закрыть окно", - "desktop.menu.undo": "Отменить", - "desktop.menu.redo": "Повторить", - "desktop.menu.cut": "Вырезать", - "desktop.menu.copy": "Копировать", - "desktop.menu.paste": "Вставить", - "desktop.menu.delete": "Удалить", - "desktop.menu.selectAll": "Выбрать всё", - "desktop.menu.toggleSidebar": "Переключить боковую панель", - "desktop.menu.toggleTerminal": "Переключить терминал", - "desktop.menu.toggleFileTree": "Переключить дерево файлов", - "desktop.menu.reload": "Перезагрузить", - "desktop.menu.toggleDeveloperTools": "Переключить инструменты разработчика", - "desktop.menu.actualSize": "Фактический размер", - "desktop.menu.zoomIn": "Увеличить", - "desktop.menu.zoomOut": "Уменьшить", - "desktop.menu.toggleFullScreen": "Переключить полноэкранный режим", - "desktop.menu.back": "Назад", - "desktop.menu.forward": "Вперёд", - "desktop.menu.previousSession": "Предыдущая сессия", - "desktop.menu.nextSession": "Следующая сессия", - "desktop.menu.previousProject": "Предыдущий проект", - "desktop.menu.nextProject": "Следующий проект", - "desktop.menu.minimize": "Свернуть", - "desktop.menu.maximize": "Развернуть", - "desktop.menu.documentation": "Документация OpenCode", - "desktop.menu.supportForum": "Форум поддержки", - "desktop.menu.shareFeedback": "Отправить отзыв", - "desktop.menu.reportBug": "Сообщить об ошибке", - "desktop.menu.ariaLabel": "Меню OpenCode", - "desktop.updater.dialog.checkFailed.message": "Не удалось проверить наличие обновлений.", - "desktop.updater.dialog.checkFailed.title": "Ошибка обновления", - "desktop.updater.dialog.upToDate.message": "У вас установлена последняя версия.", - "desktop.updater.dialog.upToDate.title": "Обновлений нет", - "desktop.updater.dialog.ready.message": "Обновление {{version}} загружено. Перезапустить сейчас?", - "desktop.updater.dialog.ready.title": "Обновление готово", - "desktop.updater.dialog.restart": "Перезапустить", - "desktop.updater.dialog.later": "Позже", - "desktop.recovery.action.relaunch": "Запустить снова", - "desktop.recovery.action.exportLogs": "Экспортировать журналы", - "desktop.recovery.action.keepWaiting": "Продолжить ожидание", - "desktop.recovery.action.quit": "Выйти", - "desktop.recovery.loadFailed": "Не удалось загрузить OpenCode", - "desktop.recovery.terminated": "Окно OpenCode неожиданно завершило работу", - "desktop.recovery.unresponsive": "OpenCode не отвечает", - "desktop.recovery.unresponsive.detail": "Можно снова запустить приложение, открыть журналы или продолжить ожидание.", - "desktop.recovery.loadFailed.detail": "Окно: {{window}}\nURL: {{url}}\nОшибка: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Окно: {{window}}\nПричина: {{reason}}\nКод: {{code}}", - "desktop.recovery.unknown": "<неизвестно>", - "desktop.dialog.chooseFolder": "Выберите папку", - "desktop.dialog.chooseFile": "Выберите файл", - "desktop.dialog.saveFile": "Сохранить файл", - "desktop.dialog.files": "Файлы", - "desktop.server.local": "Локальный сервер", - "desktop.wsl.error.windowsOnly": "WSL доступна только в Windows", - "desktop.wsl.error.unavailable": "WSL недоступна", - "desktop.wsl.error.listInstalled": "Не удалось получить список установленных дистрибутивов WSL", - "desktop.wsl.error.listOnline": "Не удалось получить список доступных дистрибутивов WSL", - "desktop.wsl.error.executeDistro": "Не удаётся выполнять команды в дистрибутиве", - "desktop.wsl.error.installWsl": "Не удалось установить WSL", - "desktop.wsl.error.installDistro": "Не удалось установить дистрибутив: {{distro}}", - "desktop.wsl.error.installOpencode": "Не удалось установить OpenCode", - "desktop.wsl.error.alreadyAdded": "Дистрибутив {{distro}} уже добавлен", - "desktop.wsl.error.opencodeMissing": "opencode не установлен в этом дистрибутиве", - "desktop.wsl.error.opencodeCannotRun": "opencode установлен, но его не удалось запустить", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode не установлен в {{distro}}", - "desktop.wsl.error.updateVersion": "Обновление OpenCode завершено, но {{distro}} по-прежнему сообщает версию {{installed}}; ожидалась {{expected}}", - "desktop.wsl.error.noVersion": "нет версии", - "desktop.wsl.error.serverExited": "Сервер WSL завершил работу после запуска (код={{code}}, сигнал={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "Сервер WSL завершил работу до перехода в рабочее состояние (код={{code}}, сигнал={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Истекло время ожидания проверки работоспособности Sidecar для {{distro}} ({{timeout}} мс)", - "desktop.wsl.error.commandTimeout": "Истекло время ожидания выполнения {{command}} {{args}} ({{timeout}} мс)", - "desktop.wsl.error.failedPort": "Не удалось получить порт", - "desktop.picker.error.notSelected": "В средстве выбора не выбран файл", - "desktop.picker.error.sizeLimit": "Размер выбранных вложений превышает лимит {{limit}} МБ", - "command.logs.export": "Экспортировать журналы", - "command.session.export": "Экспортировать сессию", - "command.session.export.description": "Экспортировать полную историю сессии в формате JSON", - "provider.connect.method.browser": "Браузер", - "provider.connect.method.headless": "Без браузера", - "provider.custom.unavailable": "Пользовательские провайдеры недоступны на этом сервере", - "help.tabs.toast.ariaLabel": "Представляем вкладки. Организуйте работу и активные сессии с помощью вкладок", - "help.tabs.toast.dismiss": "Закрыть сведения о вкладках", - "help.tabs.title": "Представляем вкладки", - "help.tabs.description": "Организуйте работу и активные сессии с помощью вкладок", - "help.tabs.date": "14 июля", - "help.tabs.introduction": "Теперь работа в OpenCode Desktop организована с помощью вкладок.", - "help.tabs.sessions": "Начните новую сессию во вкладке или откройте существующую сессию из любого проекта. Открывайте новую вкладку, когда начинаете новую задачу, и закрывайте её по завершении.", - "help.tabs.organize": "Несколько открытых вкладок упрощают организацию активных сессий. Если вы планируете оставить вкладки открытыми, присвойте им запоминающиеся названия.", - "help.tabs.home": "Все сессии и проекты находятся на новом экране «Главная». При выборе сессии она открывается во вкладке.", - "help.tabs.persistence": "После повторного открытия приложения вкладки останутся открытыми.", - "help.tabs.worktrees": "Новый дизайн пока не поддерживает Git Worktrees, но эта возможность скоро появится. Если вы предпочитаете прежний макет, переключиться между макетами можно в разделе «Настройки». Учтите, что через несколько недель новый макет станет постоянным.", - "dialog.usageExceeded.dontShowAgain": "Больше не показывать", - "context.export.session": "Экспортировать сессию", - "toast.session.export.success.title": "Сессия экспортирована", - "toast.session.export.success.description": "Сессия сохранена в файл {{filename}}", - "toast.session.export.failed.title": "Не удалось экспортировать сессию", - "toast.session.export.failed.description": "Произошла ошибка при экспорте сессии", - "session.question.pending.few": "{{count}} вопроса без ответа", - "session.question.pending.many": "{{count}} вопросов без ответа", - "session.followupDock.summary.few": "{{count}} сообщения в очереди", - "session.followupDock.summary.many": "{{count}} сообщений в очереди", - "session.revertDock.summary.few": "{{count}} сообщения возвращены", - "session.revertDock.summary.many": "{{count}} сообщений возвращено", - "terminal.connectTicket.csrfError": "Билет подключения PTY отклонён при проверке источника или CSRF. Проверьте конфигурацию CORS сервера.", - "terminal.connectTicket.statusError": "Не удалось получить билет подключения PTY, статус: {{status}}", - "titlebar.update": "Обновить", - "titlebar.updateVersion": "Обновить {{version}}", - "common.export": "Экспортировать", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Принудительно применить стили фокуса ко всем интерактивным элементам", - "debugBar.focus.on": "ВКЛ.", - "debugBar.focus.off": "ВЫКЛ.", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Принудительно задать направление макета всего приложения, не меняя выбранный язык", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts index 50a39a24..07d1ba24 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts @@ -1213,135 +1213,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "ไฟล์", - "desktop.menu.edit": "แก้ไข", - "desktop.menu.view": "มุมมอง", - "desktop.menu.go": "ไป", - "desktop.menu.window": "หน้าต่าง", - "desktop.menu.help": "วิธีใช้", - "desktop.menu.checkForUpdates": "ตรวจหาการอัปเดต...", - "desktop.menu.settings": "การตั้งค่า", - "desktop.menu.reloadWebview": "โหลด Webview ใหม่", - "desktop.menu.restart": "เริ่มการทำงานใหม่", - "desktop.menu.exportLogs": "ส่งออกบันทึก...", - "desktop.menu.newSession": "เซสชันใหม่", - "desktop.menu.openProject": "เปิดโปรเจกต์...", - "desktop.menu.newWindow": "หน้าต่างใหม่", - "desktop.menu.closeWindow": "ปิดหน้าต่าง", - "desktop.menu.undo": "เลิกทำ", - "desktop.menu.redo": "ทำซ้ำ", - "desktop.menu.cut": "ตัด", - "desktop.menu.copy": "คัดลอก", - "desktop.menu.paste": "วาง", - "desktop.menu.delete": "ลบ", - "desktop.menu.selectAll": "เลือกทั้งหมด", - "desktop.menu.toggleSidebar": "สลับแถบด้านข้าง", - "desktop.menu.toggleTerminal": "สลับเทอร์มินัล", - "desktop.menu.toggleFileTree": "สลับแผนผังไฟล์", - "desktop.menu.reload": "โหลดใหม่", - "desktop.menu.toggleDeveloperTools": "สลับเครื่องมือสำหรับนักพัฒนา", - "desktop.menu.actualSize": "ขนาดจริง", - "desktop.menu.zoomIn": "ขยาย", - "desktop.menu.zoomOut": "ย่อ", - "desktop.menu.toggleFullScreen": "สลับโหมดเต็มหน้าจอ", - "desktop.menu.back": "ย้อนกลับ", - "desktop.menu.forward": "ไปข้างหน้า", - "desktop.menu.previousSession": "เซสชันก่อนหน้า", - "desktop.menu.nextSession": "เซสชันถัดไป", - "desktop.menu.previousProject": "โปรเจกต์ก่อนหน้า", - "desktop.menu.nextProject": "โปรเจกต์ถัดไป", - "desktop.menu.minimize": "ย่อหน้าต่าง", - "desktop.menu.maximize": "ขยายหน้าต่าง", - "desktop.menu.documentation": "เอกสาร OpenCode", - "desktop.menu.supportForum": "ฟอรัมสนับสนุน", - "desktop.menu.shareFeedback": "ส่งข้อคิดเห็น", - "desktop.menu.reportBug": "รายงานข้อบกพร่อง", - "desktop.menu.ariaLabel": "เมนู OpenCode", - "desktop.updater.dialog.checkFailed.message": "การตรวจหาการอัปเดตล้มเหลว", - "desktop.updater.dialog.checkFailed.title": "ข้อผิดพลาดในการอัปเดต", - "desktop.updater.dialog.upToDate.message": "คุณกำลังใช้เวอร์ชันล่าสุด", - "desktop.updater.dialog.upToDate.title": "ไม่มีการอัปเดต", - "desktop.updater.dialog.ready.message": "ดาวน์โหลดการอัปเดต {{version}} แล้ว เริ่มการทำงานใหม่ตอนนี้หรือไม่", - "desktop.updater.dialog.ready.title": "พร้อมอัปเดต", - "desktop.updater.dialog.restart": "เริ่มการทำงานใหม่", - "desktop.updater.dialog.later": "ภายหลัง", - "desktop.recovery.action.relaunch": "เปิดอีกครั้ง", - "desktop.recovery.action.exportLogs": "ส่งออกบันทึก", - "desktop.recovery.action.keepWaiting": "รอต่อไป", - "desktop.recovery.action.quit": "ออก", - "desktop.recovery.loadFailed": "โหลด OpenCode ไม่สำเร็จ", - "desktop.recovery.terminated": "หน้าต่าง OpenCode หยุดทำงานโดยไม่คาดคิด", - "desktop.recovery.unresponsive": "OpenCode ไม่ตอบสนอง", - "desktop.recovery.unresponsive.detail": "คุณสามารถเปิดแอปอีกครั้ง เปิดบันทึก หรือรอต่อไป", - "desktop.recovery.loadFailed.detail": "หน้าต่าง: {{window}}\nURL: {{url}}\nข้อผิดพลาด: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "หน้าต่าง: {{window}}\nสาเหตุ: {{reason}}\nรหัส: {{code}}", - "desktop.recovery.unknown": "<ไม่ทราบ>", - "desktop.dialog.chooseFolder": "เลือกโฟลเดอร์", - "desktop.dialog.chooseFile": "เลือกไฟล์", - "desktop.dialog.saveFile": "บันทึกไฟล์", - "desktop.dialog.files": "ไฟล์", - "desktop.server.local": "เซิร์ฟเวอร์ภายในเครื่อง", - "desktop.wsl.error.windowsOnly": "WSL ใช้งานได้เฉพาะบน Windows เท่านั้น", - "desktop.wsl.error.unavailable": "WSL ไม่พร้อมใช้งาน", - "desktop.wsl.error.listInstalled": "ไม่สามารถแสดงรายการดิสโทร WSL ที่ติดตั้งแล้วได้", - "desktop.wsl.error.listOnline": "ไม่สามารถแสดงรายการดิสโทร WSL แบบออนไลน์ได้", - "desktop.wsl.error.executeDistro": "ไม่สามารถเรียกใช้คำสั่งในดิสโทรได้", - "desktop.wsl.error.installWsl": "การติดตั้ง WSL ล้มเหลว", - "desktop.wsl.error.installDistro": "ไม่สามารถติดตั้งดิสโทรได้: {{distro}}", - "desktop.wsl.error.installOpencode": "การติดตั้ง OpenCode ล้มเหลว", - "desktop.wsl.error.alreadyAdded": "เพิ่ม {{distro}} แล้ว", - "desktop.wsl.error.opencodeMissing": "ไม่ได้ติดตั้ง opencode ในดิสโทรนี้", - "desktop.wsl.error.opencodeCannotRun": "ติดตั้ง opencode แล้ว แต่ไม่สามารถเรียกใช้ได้", - "desktop.wsl.error.opencodeNotInstalled": "ไม่ได้ติดตั้ง OpenCode ใน {{distro}}", - "desktop.wsl.error.updateVersion": "อัปเดต OpenCode เสร็จแล้ว แต่ {{distro}} ยังคงรายงานเวอร์ชัน {{installed}} ทั้งที่ควรเป็น {{expected}}", - "desktop.wsl.error.noVersion": "ไม่มีเวอร์ชัน", - "desktop.wsl.error.serverExited": "เซิร์ฟเวอร์ WSL หยุดทำงานหลังเริ่มต้น (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "เซิร์ฟเวอร์ WSL หยุดทำงานก่อนพร้อมใช้งาน (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "การตรวจสอบสถานะ Sidecar สำหรับ {{distro}} หมดเวลาหลังจาก {{timeout}}ms", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} หมดเวลาหลังจาก {{timeout}}ms", - "desktop.wsl.error.failedPort": "ไม่สามารถรับพอร์ตได้", - "desktop.picker.error.notSelected": "ไม่ได้เลือกไฟล์จากตัวเลือกไฟล์", - "desktop.picker.error.sizeLimit": "ไฟล์แนบที่เลือกมีขนาดเกินขีดจำกัด {{limit}} MB", - "command.logs.export": "ส่งออกบันทึก", - "command.session.export": "ส่งออกเซสชัน", - "command.session.export.description": "ส่งออกบันทึกทั้งหมดของเซสชันเป็น JSON", - "provider.connect.method.browser": "เบราว์เซอร์", - "provider.connect.method.headless": "ไม่ใช้เบราว์เซอร์", - "provider.custom.unavailable": "ผู้ให้บริการที่กำหนดเองไม่พร้อมใช้งานบนเซิร์ฟเวอร์นี้", - "help.tabs.toast.ariaLabel": "ขอแนะนำแท็บ จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ", - "help.tabs.toast.dismiss": "ปิดข้อมูลเกี่ยวกับแท็บ", - "help.tabs.title": "ขอแนะนำแท็บ", - "help.tabs.description": "จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ", - "help.tabs.date": "14 กรกฎาคม", - "help.tabs.introduction": "ตอนนี้ OpenCode Desktop สร้างขึ้นโดยใช้แท็บเป็นหลัก", - "help.tabs.sessions": "เริ่มเซสชันใหม่ในแท็บ หรือเปิดเซสชันที่มีอยู่จากโปรเจกต์ใดก็ได้ เปิดแท็บใหม่เมื่อเริ่มงานใหม่ และปิดเมื่อทำงานเสร็จ", - "help.tabs.organize": "การเปิดแท็บไว้สองสามแท็บช่วยให้จัดระเบียบเซสชันที่ใช้งานอยู่ได้ง่ายขึ้น เปลี่ยนชื่อแท็บให้จดจำได้ง่ายหากต้องการเก็บแท็บไว้", - "help.tabs.home": "คุณจะพบเซสชันและโปรเจกต์ทั้งหมดในหน้าจอหน้าหลักแบบใหม่ เมื่อเลือกเซสชัน เซสชันนั้นจะเปิดในแท็บ", - "help.tabs.persistence": "เมื่อเปิดแอปอีกครั้ง แท็บของคุณจะยังคงเปิดอยู่", - "help.tabs.worktrees": "การออกแบบใหม่ยังไม่รองรับ Git Worktrees แต่จะรองรับเร็ว ๆ นี้ ดังนั้น หากต้องการใช้เค้าโครงก่อนหน้าต่อ คุณสามารถสลับเค้าโครงได้ในการตั้งค่า โปรดทราบว่าเค้าโครงใหม่จะกลายเป็นเค้าโครงถาวรภายในอีกไม่กี่สัปดาห์", - "dialog.usageExceeded.dontShowAgain": "ไม่ต้องแสดงอีก", - "context.export.session": "ส่งออกเซสชัน", - "toast.session.export.success.title": "ส่งออกเซสชันแล้ว", - "toast.session.export.success.description": "บันทึกเซสชันไปยัง {{filename}} แล้ว", - "toast.session.export.failed.title": "ไม่สามารถส่งออกเซสชัน", - "toast.session.export.failed.description": "เกิดข้อผิดพลาดขณะส่งออกเซสชัน", - "terminal.connectTicket.csrfError": "ตั๋วเชื่อมต่อ PTY ถูกปฏิเสธโดยการตรวจสอบต้นทางหรือ CSRF โปรดตรวจสอบการกำหนดค่า CORS ของเซิร์ฟเวอร์", - "terminal.connectTicket.statusError": "ตั๋วเชื่อมต่อ PTY ล้มเหลวโดยมีสถานะ {{status}}", - "titlebar.update": "อัปเดต", - "titlebar.updateVersion": "อัปเดต {{version}}", - "common.export": "ส่งออก", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "บังคับใช้สไตล์โฟกัสกับองค์ประกอบแบบโต้ตอบทั้งหมด", - "debugBar.focus.on": "เปิด", - "debugBar.focus.off": "ปิด", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "บังคับกำหนดทิศทางเค้าโครงของทั้งแอปโดยไม่เปลี่ยนภาษาที่เลือก", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts index d25a3daa..09ab55fb 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts @@ -1232,135 +1232,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Dosya", - "desktop.menu.edit": "Düzen", - "desktop.menu.view": "Görünüm", - "desktop.menu.go": "Git", - "desktop.menu.window": "Pencere", - "desktop.menu.help": "Yardım", - "desktop.menu.checkForUpdates": "Güncellemeleri kontrol et...", - "desktop.menu.settings": "Ayarlar", - "desktop.menu.reloadWebview": "Web görünümünü yeniden yükle", - "desktop.menu.restart": "Yeniden başlat", - "desktop.menu.exportLogs": "Günlükleri dışa aktar...", - "desktop.menu.newSession": "Yeni oturum", - "desktop.menu.openProject": "Proje aç...", - "desktop.menu.newWindow": "Yeni pencere", - "desktop.menu.closeWindow": "Pencereyi kapat", - "desktop.menu.undo": "Geri al", - "desktop.menu.redo": "Yinele", - "desktop.menu.cut": "Kes", - "desktop.menu.copy": "Kopyala", - "desktop.menu.paste": "Yapıştır", - "desktop.menu.delete": "Sil", - "desktop.menu.selectAll": "Tümünü seç", - "desktop.menu.toggleSidebar": "Kenar çubuğunu aç/kapat", - "desktop.menu.toggleTerminal": "Terminali aç/kapat", - "desktop.menu.toggleFileTree": "Dosya ağacını aç/kapat", - "desktop.menu.reload": "Yeniden yükle", - "desktop.menu.toggleDeveloperTools": "Geliştirici araçlarını aç/kapat", - "desktop.menu.actualSize": "Gerçek boyut", - "desktop.menu.zoomIn": "Yakınlaştır", - "desktop.menu.zoomOut": "Uzaklaştır", - "desktop.menu.toggleFullScreen": "Tam ekranı aç/kapat", - "desktop.menu.back": "Geri", - "desktop.menu.forward": "İleri", - "desktop.menu.previousSession": "Önceki oturum", - "desktop.menu.nextSession": "Sonraki oturum", - "desktop.menu.previousProject": "Önceki proje", - "desktop.menu.nextProject": "Sonraki proje", - "desktop.menu.minimize": "Simge durumuna küçült", - "desktop.menu.maximize": "Ekranı kapla", - "desktop.menu.documentation": "OpenCode belgeleri", - "desktop.menu.supportForum": "Destek forumu", - "desktop.menu.shareFeedback": "Geri bildirim paylaş", - "desktop.menu.reportBug": "Hata bildir", - "desktop.menu.ariaLabel": "OpenCode menüsü", - "desktop.updater.dialog.checkFailed.message": "Güncellemeler kontrol edilemedi.", - "desktop.updater.dialog.checkFailed.title": "Güncelleme hatası", - "desktop.updater.dialog.upToDate.message": "En son sürümü kullanıyorsunuz.", - "desktop.updater.dialog.upToDate.title": "Güncelleme yok", - "desktop.updater.dialog.ready.message": "{{version}} güncellemesi indirildi. Şimdi yeniden başlatılsın mı?", - "desktop.updater.dialog.ready.title": "Güncelleme hazır", - "desktop.updater.dialog.restart": "Yeniden başlat", - "desktop.updater.dialog.later": "Daha sonra", - "desktop.recovery.action.relaunch": "Uygulamayı yeniden başlat", - "desktop.recovery.action.exportLogs": "Günlükleri dışa aktar", - "desktop.recovery.action.keepWaiting": "Beklemeye devam et", - "desktop.recovery.action.quit": "Çık", - "desktop.recovery.loadFailed": "OpenCode yüklenemedi", - "desktop.recovery.terminated": "OpenCode penceresi beklenmedik şekilde sonlandırıldı", - "desktop.recovery.unresponsive": "OpenCode yanıt vermiyor", - "desktop.recovery.unresponsive.detail": "Uygulamayı yeniden başlatabilir, günlükleri açabilir veya beklemeye devam edebilirsiniz.", - "desktop.recovery.loadFailed.detail": "Pencere: {{window}}\nURL: {{url}}\nHata: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Pencere: {{window}}\nNeden: {{reason}}\nKod: {{code}}", - "desktop.recovery.unknown": "", - "desktop.dialog.chooseFolder": "Bir klasör seçin", - "desktop.dialog.chooseFile": "Bir dosya seçin", - "desktop.dialog.saveFile": "Dosyayı kaydedin", - "desktop.dialog.files": "Dosyalar", - "desktop.server.local": "Yerel sunucu", - "desktop.wsl.error.windowsOnly": "WSL yalnızca Windows'ta kullanılabilir", - "desktop.wsl.error.unavailable": "WSL kullanılamıyor", - "desktop.wsl.error.listInstalled": "Kurulu WSL dağıtımları listelenemedi", - "desktop.wsl.error.listOnline": "Çevrimiçi WSL dağıtımları listelenemedi", - "desktop.wsl.error.executeDistro": "Dağıtımda komutlar çalıştırılamıyor", - "desktop.wsl.error.installWsl": "WSL kurulamadı", - "desktop.wsl.error.installDistro": "Dağıtım kurulamadı: {{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode kurulamadı", - "desktop.wsl.error.alreadyAdded": "{{distro}} zaten eklendi", - "desktop.wsl.error.opencodeMissing": "opencode bu dağıtımda kurulu değil", - "desktop.wsl.error.opencodeCannotRun": "opencode kurulu ancak çalıştırılamadı", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode, {{distro}} içinde kurulu değil", - "desktop.wsl.error.updateVersion": "OpenCode güncellemesi tamamlandı ancak {{distro}} hâlâ {{installed}} sürümünü bildiriyor; beklenen sürüm: {{expected}}", - "desktop.wsl.error.noVersion": "sürüm yok", - "desktop.wsl.error.serverExited": "WSL sunucusu başlatıldıktan sonra kapandı (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL sunucusu kullanıma hazır duruma gelmeden kapandı (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}} Sidecar sistem durumu denetimi {{timeout}} ms sonra zaman aşımına uğradı", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} komutu {{timeout}} ms sonra zaman aşımına uğradı", - "desktop.wsl.error.failedPort": "Bağlantı noktası alınamadı", - "desktop.picker.error.notSelected": "Dosya seçici tarafından dosya seçilmedi", - "desktop.picker.error.sizeLimit": "Seçilen ekler {{limit}} MB sınırını aşıyor", - "command.logs.export": "Günlükleri dışa aktar", - "command.session.export": "Oturumu dışa aktar", - "command.session.export.description": "Oturumun tam dökümünü JSON olarak dışa aktar", - "provider.connect.method.browser": "Tarayıcı", - "provider.connect.method.headless": "Tarayıcısız", - "provider.custom.unavailable": "Özel sağlayıcılar bu sunucuda kullanılamıyor", - "help.tabs.toast.ariaLabel": "Sekmelerle tanışın. Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin", - "help.tabs.toast.dismiss": "Sekmeler bilgilerini kapat", - "help.tabs.title": "Sekmelerle tanışın", - "help.tabs.description": "Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin", - "help.tabs.date": "14 Temmuz", - "help.tabs.introduction": "OpenCode Desktop artık sekmeler üzerine kurulu.", - "help.tabs.sessions": "Bir sekmede yeni oturum başlatın veya projelerinizden mevcut bir oturumu açın. Yeni bir işe başlarken yeni sekme açın, işiniz bittiğinde sekmeyi kapatın.", - "help.tabs.organize": "Birkaç sekmeyi açık tutmak, etkin oturumlarınızı düzenlemenizi kolaylaştırır. Sekmeleri açık tutmayı planlıyorsanız onlara akılda kalıcı adlar verin.", - "help.tabs.home": "Tüm oturumlarınızı ve projelerinizi yeni Ana Sayfa ekranında bulabilirsiniz. Bir oturum seçtiğinizde oturum bir sekmede açılır.", - "help.tabs.persistence": "Uygulamayı yeniden açtığınızda sekmeleriniz açık kalır.", - "help.tabs.worktrees": "Yeni tasarım henüz Git Worktrees'i desteklemiyor; bu özellik yakında kullanıma sunulacak. Önceki düzeni kullanmaya devam etmek isterseniz Ayarlar'da düzenler arasında geçiş yapabilirsiniz. Yeni düzenin birkaç hafta içinde kalıcı olacağını unutmayın.", - "dialog.usageExceeded.dontShowAgain": "Bir daha gösterme", - "context.export.session": "Oturumu dışa aktar", - "toast.session.export.success.title": "Oturum dışa aktarıldı", - "toast.session.export.success.description": "Oturum {{filename}} dosyasına kaydedildi", - "toast.session.export.failed.title": "Oturum dışa aktarılamadı", - "toast.session.export.failed.description": "Oturum dışa aktarılırken bir hata oluştu", - "terminal.connectTicket.csrfError": "PTY bağlantı bileti, kaynak veya CSRF denetimleri tarafından reddedildi. Sunucunun CORS yapılandırmasını kontrol edin.", - "terminal.connectTicket.statusError": "PTY bağlantı bileti {{status}} durumuyla başarısız oldu", - "titlebar.update": "Güncelle", - "titlebar.updateVersion": "Güncelle {{version}}", - "common.export": "Dışa aktar", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Tüm etkileşimli öğelere odak stillerini zorla uygula", - "debugBar.focus.on": "AÇIK", - "debugBar.focus.off": "KAPALI", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Seçili dili değiştirmeden tüm uygulamanın düzen yönünü zorla ayarla", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts index 8577455d..43aa325a 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts @@ -1231,141 +1231,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "Файл", - "desktop.menu.edit": "Редагування", - "desktop.menu.view": "Вигляд", - "desktop.menu.go": "Перехід", - "desktop.menu.window": "Вікно", - "desktop.menu.help": "Довідка", - "desktop.menu.checkForUpdates": "Перевірити наявність оновлень...", - "desktop.menu.settings": "Налаштування", - "desktop.menu.reloadWebview": "Перезавантажити вебподання", - "desktop.menu.restart": "Перезапустити", - "desktop.menu.exportLogs": "Експортувати журнали...", - "desktop.menu.newSession": "Нова сесія", - "desktop.menu.openProject": "Відкрити проєкт...", - "desktop.menu.newWindow": "Нове вікно", - "desktop.menu.closeWindow": "Закрити вікно", - "desktop.menu.undo": "Скасувати", - "desktop.menu.redo": "Повторити", - "desktop.menu.cut": "Вирізати", - "desktop.menu.copy": "Копіювати", - "desktop.menu.paste": "Вставити", - "desktop.menu.delete": "Видалити", - "desktop.menu.selectAll": "Вибрати все", - "desktop.menu.toggleSidebar": "Перемкнути бічну панель", - "desktop.menu.toggleTerminal": "Перемкнути термінал", - "desktop.menu.toggleFileTree": "Перемкнути дерево файлів", - "desktop.menu.reload": "Перезавантажити", - "desktop.menu.toggleDeveloperTools": "Перемкнути інструменти розробника", - "desktop.menu.actualSize": "Фактичний розмір", - "desktop.menu.zoomIn": "Збільшити", - "desktop.menu.zoomOut": "Зменшити", - "desktop.menu.toggleFullScreen": "Перемкнути повноекранний режим", - "desktop.menu.back": "Назад", - "desktop.menu.forward": "Вперед", - "desktop.menu.previousSession": "Попередня сесія", - "desktop.menu.nextSession": "Наступна сесія", - "desktop.menu.previousProject": "Попередній проєкт", - "desktop.menu.nextProject": "Наступний проєкт", - "desktop.menu.minimize": "Згорнути", - "desktop.menu.maximize": "Розгорнути", - "desktop.menu.documentation": "Документація OpenCode", - "desktop.menu.supportForum": "Форум підтримки", - "desktop.menu.shareFeedback": "Надіслати відгук", - "desktop.menu.reportBug": "Повідомити про помилку", - "desktop.menu.ariaLabel": "Меню OpenCode", - "desktop.updater.dialog.checkFailed.message": "Не вдалося перевірити наявність оновлень.", - "desktop.updater.dialog.checkFailed.title": "Помилка оновлення", - "desktop.updater.dialog.upToDate.message": "У вас найновіша версія.", - "desktop.updater.dialog.upToDate.title": "Оновлень немає", - "desktop.updater.dialog.ready.message": "Оновлення {{version}} завантажено. Перезапустити зараз?", - "desktop.updater.dialog.ready.title": "Оновлення готове", - "desktop.updater.dialog.restart": "Перезапустити", - "desktop.updater.dialog.later": "Пізніше", - "desktop.recovery.action.relaunch": "Запустити повторно", - "desktop.recovery.action.exportLogs": "Експортувати журнали", - "desktop.recovery.action.keepWaiting": "Продовжити очікування", - "desktop.recovery.action.quit": "Завершити роботу", - "desktop.recovery.loadFailed": "Не вдалося завантажити OpenCode", - "desktop.recovery.terminated": "Роботу вікна OpenCode несподівано завершено", - "desktop.recovery.unresponsive": "OpenCode не відповідає", - "desktop.recovery.unresponsive.detail": "Ви можете повторно запустити програму, відкрити журнали або продовжити очікування.", - "desktop.recovery.loadFailed.detail": "Вікно: {{window}}\nURL: {{url}}\nПомилка: {{code}} {{description}}", - "desktop.recovery.terminated.detail": "Вікно: {{window}}\nПричина: {{reason}}\nКод: {{code}}", - "desktop.recovery.unknown": "<невідомо>", - "desktop.dialog.chooseFolder": "Виберіть папку", - "desktop.dialog.chooseFile": "Виберіть файл", - "desktop.dialog.saveFile": "Зберегти файл", - "desktop.dialog.files": "Файли", - "desktop.server.local": "Локальний сервер", - "desktop.wsl.error.windowsOnly": "WSL доступна лише у Windows", - "desktop.wsl.error.unavailable": "WSL недоступна", - "desktop.wsl.error.listInstalled": "Не вдалося отримати список встановлених дистрибутивів WSL", - "desktop.wsl.error.listOnline": "Не вдалося отримати список доступних онлайн дистрибутивів WSL", - "desktop.wsl.error.executeDistro": "Не вдалося виконати команди в дистрибутиві", - "desktop.wsl.error.installWsl": "Не вдалося встановити WSL", - "desktop.wsl.error.installDistro": "Не вдалося встановити дистрибутив: {{distro}}", - "desktop.wsl.error.installOpencode": "Не вдалося встановити OpenCode", - "desktop.wsl.error.alreadyAdded": "{{distro}} уже додано", - "desktop.wsl.error.opencodeMissing": "opencode не встановлено в цьому дистрибутиві", - "desktop.wsl.error.opencodeCannotRun": "opencode встановлено, але його не вдалося запустити", - "desktop.wsl.error.opencodeNotInstalled": "OpenCode не встановлено в {{distro}}", - "desktop.wsl.error.updateVersion": "Оновлення OpenCode завершено, але {{distro}} усе ще повідомляє про версію {{installed}}; очікувалася {{expected}}", - "desktop.wsl.error.noVersion": "версію не вказано", - "desktop.wsl.error.serverExited": "Сервер WSL завершив роботу після запуску (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "Сервер WSL завершив роботу, не досягнувши працездатного стану (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "Час очікування перевірки працездатності sidecar для {{distro}} минув через {{timeout}}мс", - "desktop.wsl.error.commandTimeout": "Час очікування {{command}} {{args}} минув через {{timeout}}мс", - "desktop.wsl.error.failedPort": "Не вдалося отримати порт", - "desktop.picker.error.notSelected": "Засіб вибору не повернув вибраний файл", - "desktop.picker.error.sizeLimit": "Вибрані вкладення перевищують обмеження в {{limit}} МБ", - "command.logs.export": "Експортувати журнали", - "command.session.export": "Експортувати сесію", - "command.session.export.description": "Експортувати повну історію сесії у форматі JSON", - "provider.connect.method.browser": "Браузер", - "provider.connect.method.headless": "Без графічного інтерфейсу", - "provider.custom.unavailable": "Користувацькі провайдери недоступні на цьому сервері", - "help.tabs.toast.ariaLabel": "Представляємо вкладки. Упорядковуйте роботу й активні сесії за допомогою вкладок", - "help.tabs.toast.dismiss": "Закрити інформацію про вкладки", - "help.tabs.title": "Представляємо вкладки", - "help.tabs.description": "Упорядковуйте роботу й активні сесії за допомогою вкладок", - "help.tabs.date": "14 липня", - "help.tabs.introduction": "OpenCode Desktop тепер побудовано навколо вкладок.", - "help.tabs.sessions": "Почніть нову сесію у вкладці або відкрийте наявну сесію з будь-якого свого проєкту. Відкривайте нову вкладку, коли починаєте щось нове, і закривайте її після завершення.", - "help.tabs.organize": "Кілька відкритих вкладок полегшують упорядкування активних сесій. Якщо плануєте залишити вкладки відкритими, перейменуйте їх на щось пам'ятне.", - "help.tabs.home": "Усі свої сесії та проєкти ви знайдете на новому екрані «Головна». Вибрана сесія відкриється у вкладці.", - "help.tabs.persistence": "Після повторного відкриття програми ваші вкладки залишаться відкритими.", - "help.tabs.worktrees": "Новий дизайн поки не підтримує робочі дерева Git, але ця можливість незабаром з'явиться. Тож якщо ви бажаєте й надалі користуватися попереднім макетом, перемикатися між макетами можна в налаштуваннях. Зауважте, що за кілька тижнів новий макет стане постійним.", - "dialog.usageExceeded.dontShowAgain": "Більше не показувати", - "context.export.session": "Експортувати сесію", - "toast.session.export.success.title": "Сесію експортовано", - "toast.session.export.success.description": "Сесію збережено у файл {{filename}}", - "toast.session.export.failed.title": "Не вдалося експортувати сесію", - "toast.session.export.failed.description": "Під час експортування сесії сталася помилка", - "session.question.pending.few": "{{count}} запитання очікують відповіді", - "session.question.pending.many": "{{count}} запитань очікують відповіді", - "session.followupDock.summary.few": "{{count}} повідомлення в черзі", - "session.followupDock.summary.many": "{{count}} повідомлень у черзі", - "session.revertDock.summary.few": "{{count}} скасовані повідомлення", - "session.revertDock.summary.many": "{{count}} скасованих повідомлень", - "terminal.connectTicket.csrfError": "Квиток підключення PTY відхилено через перевірку джерела або CSRF. Перевірте конфігурацію CORS сервера.", - "terminal.connectTicket.statusError": "Помилка квитка підключення PTY зі статусом {{status}}", - "titlebar.update": "Оновити", - "titlebar.updateVersion": "Оновити {{version}}", - "common.export": "Експортувати", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "Примусово застосувати стилі фокуса до всіх інтерактивних елементів", - "debugBar.focus.on": "УВІМК.", - "debugBar.focus.off": "ВИМК.", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "Примусово задати напрямок макета всієї програми, не змінюючи вибрану мову", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts index edb37496..adfcff20 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts @@ -1204,135 +1204,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "文件", - "desktop.menu.edit": "编辑", - "desktop.menu.view": "视图", - "desktop.menu.go": "转到", - "desktop.menu.window": "窗口", - "desktop.menu.help": "帮助", - "desktop.menu.checkForUpdates": "检查更新...", - "desktop.menu.settings": "设置", - "desktop.menu.reloadWebview": "重新加载 Webview", - "desktop.menu.restart": "重启", - "desktop.menu.exportLogs": "导出日志...", - "desktop.menu.newSession": "新建会话", - "desktop.menu.openProject": "打开项目...", - "desktop.menu.newWindow": "新建窗口", - "desktop.menu.closeWindow": "关闭窗口", - "desktop.menu.undo": "撤销", - "desktop.menu.redo": "重做", - "desktop.menu.cut": "剪切", - "desktop.menu.copy": "复制", - "desktop.menu.paste": "粘贴", - "desktop.menu.delete": "删除", - "desktop.menu.selectAll": "全选", - "desktop.menu.toggleSidebar": "切换侧边栏", - "desktop.menu.toggleTerminal": "切换终端", - "desktop.menu.toggleFileTree": "切换文件树", - "desktop.menu.reload": "重新加载", - "desktop.menu.toggleDeveloperTools": "切换开发人员工具", - "desktop.menu.actualSize": "实际大小", - "desktop.menu.zoomIn": "放大", - "desktop.menu.zoomOut": "缩小", - "desktop.menu.toggleFullScreen": "切换全屏显示", - "desktop.menu.back": "后退", - "desktop.menu.forward": "前进", - "desktop.menu.previousSession": "上一个会话", - "desktop.menu.nextSession": "下一个会话", - "desktop.menu.previousProject": "上一个项目", - "desktop.menu.nextProject": "下一个项目", - "desktop.menu.minimize": "最小化", - "desktop.menu.maximize": "最大化", - "desktop.menu.documentation": "OpenCode 文档", - "desktop.menu.supportForum": "支持论坛", - "desktop.menu.shareFeedback": "提供反馈", - "desktop.menu.reportBug": "报告错误", - "desktop.menu.ariaLabel": "OpenCode 菜单", - "desktop.updater.dialog.checkFailed.message": "检查更新失败。", - "desktop.updater.dialog.checkFailed.title": "更新错误", - "desktop.updater.dialog.upToDate.message": "你使用的是最新版本。", - "desktop.updater.dialog.upToDate.title": "无可用更新", - "desktop.updater.dialog.ready.message": "已下载更新 {{version}}。是否立即重启?", - "desktop.updater.dialog.ready.title": "更新已就绪", - "desktop.updater.dialog.restart": "重启", - "desktop.updater.dialog.later": "稍后", - "desktop.recovery.action.relaunch": "重新启动", - "desktop.recovery.action.exportLogs": "导出日志", - "desktop.recovery.action.keepWaiting": "继续等待", - "desktop.recovery.action.quit": "退出", - "desktop.recovery.loadFailed": "OpenCode 加载失败", - "desktop.recovery.terminated": "OpenCode 窗口意外终止", - "desktop.recovery.unresponsive": "OpenCode 未响应", - "desktop.recovery.unresponsive.detail": "你可以重新启动应用、打开日志或继续等待。", - "desktop.recovery.loadFailed.detail": "窗口:{{window}}\nURL:{{url}}\n错误:{{code}} {{description}}", - "desktop.recovery.terminated.detail": "窗口:{{window}}\n原因:{{reason}}\n代码:{{code}}", - "desktop.recovery.unknown": "<未知>", - "desktop.dialog.chooseFolder": "选择文件夹", - "desktop.dialog.chooseFile": "选择文件", - "desktop.dialog.saveFile": "保存文件", - "desktop.dialog.files": "文件", - "desktop.server.local": "本地服务器", - "desktop.wsl.error.windowsOnly": "WSL 仅在 Windows 上可用", - "desktop.wsl.error.unavailable": "WSL 不可用", - "desktop.wsl.error.listInstalled": "无法列出已安装的 WSL 发行版", - "desktop.wsl.error.listOnline": "无法列出在线 WSL 发行版", - "desktop.wsl.error.executeDistro": "无法在发行版中执行命令", - "desktop.wsl.error.installWsl": "WSL 安装失败", - "desktop.wsl.error.installDistro": "无法安装发行版:{{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode 安装失败", - "desktop.wsl.error.alreadyAdded": "已添加 {{distro}}", - "desktop.wsl.error.opencodeMissing": "此发行版中未安装 opencode", - "desktop.wsl.error.opencodeCannotRun": "已安装 opencode,但无法运行", - "desktop.wsl.error.opencodeNotInstalled": "{{distro}} 中未安装 OpenCode", - "desktop.wsl.error.updateVersion": "OpenCode 更新已完成,但 {{distro}} 仍报告版本 {{installed}};预期版本为 {{expected}}", - "desktop.wsl.error.noVersion": "无版本信息", - "desktop.wsl.error.serverExited": "WSL 服务器启动后退出(code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL 服务器在进入正常状态前退出(code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}} 的 Sidecar 运行状况检查在 {{timeout}}ms 后超时", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 在 {{timeout}}ms 后超时", - "desktop.wsl.error.failedPort": "无法获取端口", - "desktop.picker.error.notSelected": "文件选择器未选择文件", - "desktop.picker.error.sizeLimit": "所选附件超过 {{limit}} MB 的限制", - "command.logs.export": "导出日志", - "command.session.export": "导出会话", - "command.session.export.description": "将完整会话记录导出为 JSON", - "provider.connect.method.browser": "浏览器", - "provider.connect.method.headless": "无头模式", - "provider.custom.unavailable": "此服务器上无法使用自定义提供商", - "help.tabs.toast.ariaLabel": "标签页功能介绍。使用标签页整理工作和进行中的会话", - "help.tabs.toast.dismiss": "关闭标签页功能介绍", - "help.tabs.title": "标签页功能介绍", - "help.tabs.description": "使用标签页整理工作和进行中的会话", - "help.tabs.date": "7 月 14 日", - "help.tabs.introduction": "OpenCode Desktop 现已采用以标签页为核心的设计。", - "help.tabs.sessions": "在标签页中开始新会话,或从任意项目打开现有会话。开始处理新任务时打开新标签页,完成后将其关闭。", - "help.tabs.organize": "保持打开几个标签页,可更轻松地整理进行中的会话。如果打算保留某个标签页,请为其指定一个便于记忆的名称。", - "help.tabs.home": "你可以在新的主页中找到所有会话和项目。选择会话即可在标签页中将其打开。", - "help.tabs.persistence": "重新打开应用时,标签页仍会保持打开。", - "help.tabs.worktrees": "新设计暂不支持 Git Worktrees,此功能即将推出。如果希望继续使用之前的布局,可在“设置”中切换布局。请注意,新布局将在几周后永久启用。", - "dialog.usageExceeded.dontShowAgain": "不再显示", - "context.export.session": "导出会话", - "toast.session.export.success.title": "会话已导出", - "toast.session.export.success.description": "已将会话保存到 {{filename}}", - "toast.session.export.failed.title": "导出会话失败", - "toast.session.export.failed.description": "导出会话时发生错误", - "terminal.connectTicket.csrfError": "PTY 连接票证因来源或 CSRF 检查被拒绝。请检查服务器 CORS 配置。", - "terminal.connectTicket.statusError": "PTY 连接票证失败,状态为 {{status}}", - "titlebar.update": "更新", - "titlebar.updateVersion": "更新 {{version}}", - "common.export": "导出", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "强制所有交互元素显示焦点样式", - "debugBar.focus.on": "开", - "debugBar.focus.off": "关", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "在不更改所选语言的情况下,强制设置整个应用的布局方向", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts index 522517e4..83bc7124 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts @@ -1200,135 +1200,4 @@ export const dict = { "settings.skills.action.add_directory": "Add directory", "settings.skills.action.remove": "Remove", "settings.skills.empty": "No custom skill providers configured. Add a directory containing skills to get started.", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "desktop.menu.app": "OpenCode", - "desktop.menu.file": "檔案", - "desktop.menu.edit": "編輯", - "desktop.menu.view": "檢視", - "desktop.menu.go": "前往", - "desktop.menu.window": "視窗", - "desktop.menu.help": "說明", - "desktop.menu.checkForUpdates": "檢查更新...", - "desktop.menu.settings": "設定", - "desktop.menu.reloadWebview": "重新載入 Webview", - "desktop.menu.restart": "重新啟動", - "desktop.menu.exportLogs": "匯出記錄檔...", - "desktop.menu.newSession": "新增工作階段", - "desktop.menu.openProject": "開啟專案...", - "desktop.menu.newWindow": "新增視窗", - "desktop.menu.closeWindow": "關閉視窗", - "desktop.menu.undo": "復原", - "desktop.menu.redo": "重做", - "desktop.menu.cut": "剪下", - "desktop.menu.copy": "複製", - "desktop.menu.paste": "貼上", - "desktop.menu.delete": "刪除", - "desktop.menu.selectAll": "全選", - "desktop.menu.toggleSidebar": "切換側邊欄", - "desktop.menu.toggleTerminal": "切換終端機", - "desktop.menu.toggleFileTree": "切換檔案樹", - "desktop.menu.reload": "重新載入", - "desktop.menu.toggleDeveloperTools": "切換開發人員工具", - "desktop.menu.actualSize": "實際大小", - "desktop.menu.zoomIn": "放大", - "desktop.menu.zoomOut": "縮小", - "desktop.menu.toggleFullScreen": "切換全螢幕", - "desktop.menu.back": "上一頁", - "desktop.menu.forward": "下一頁", - "desktop.menu.previousSession": "上一個工作階段", - "desktop.menu.nextSession": "下一個工作階段", - "desktop.menu.previousProject": "上一個專案", - "desktop.menu.nextProject": "下一個專案", - "desktop.menu.minimize": "最小化", - "desktop.menu.maximize": "最大化", - "desktop.menu.documentation": "OpenCode 文件", - "desktop.menu.supportForum": "支援論壇", - "desktop.menu.shareFeedback": "提供意見回饋", - "desktop.menu.reportBug": "回報錯誤", - "desktop.menu.ariaLabel": "OpenCode 選單", - "desktop.updater.dialog.checkFailed.message": "檢查更新失敗。", - "desktop.updater.dialog.checkFailed.title": "更新錯誤", - "desktop.updater.dialog.upToDate.message": "目前已是最新版本。", - "desktop.updater.dialog.upToDate.title": "沒有更新", - "desktop.updater.dialog.ready.message": "已下載更新 {{version}}。要立即重新啟動嗎?", - "desktop.updater.dialog.ready.title": "更新已就緒", - "desktop.updater.dialog.restart": "重新啟動", - "desktop.updater.dialog.later": "稍後", - "desktop.recovery.action.relaunch": "重新啟動應用程式", - "desktop.recovery.action.exportLogs": "匯出記錄檔", - "desktop.recovery.action.keepWaiting": "繼續等待", - "desktop.recovery.action.quit": "結束", - "desktop.recovery.loadFailed": "OpenCode 載入失敗", - "desktop.recovery.terminated": "OpenCode 視窗意外終止", - "desktop.recovery.unresponsive": "OpenCode 沒有回應", - "desktop.recovery.unresponsive.detail": "你可以重新啟動應用程式、開啟記錄檔,或繼續等待。", - "desktop.recovery.loadFailed.detail": "視窗:{{window}}\nURL:{{url}}\n錯誤:{{code}} {{description}}", - "desktop.recovery.terminated.detail": "視窗:{{window}}\n原因:{{reason}}\n代碼:{{code}}", - "desktop.recovery.unknown": "<未知>", - "desktop.dialog.chooseFolder": "選擇資料夾", - "desktop.dialog.chooseFile": "選擇檔案", - "desktop.dialog.saveFile": "儲存檔案", - "desktop.dialog.files": "檔案", - "desktop.server.local": "本機伺服器", - "desktop.wsl.error.windowsOnly": "WSL 僅適用於 Windows", - "desktop.wsl.error.unavailable": "WSL 無法使用", - "desktop.wsl.error.listInstalled": "無法列出已安裝的 WSL 發行版", - "desktop.wsl.error.listOnline": "無法列出線上 WSL 發行版", - "desktop.wsl.error.executeDistro": "無法在發行版中執行命令", - "desktop.wsl.error.installWsl": "WSL 安裝失敗", - "desktop.wsl.error.installDistro": "無法安裝發行版:{{distro}}", - "desktop.wsl.error.installOpencode": "OpenCode 安裝失敗", - "desktop.wsl.error.alreadyAdded": "已新增 {{distro}}", - "desktop.wsl.error.opencodeMissing": "此發行版中未安裝 opencode", - "desktop.wsl.error.opencodeCannotRun": "已安裝 opencode,但無法執行", - "desktop.wsl.error.opencodeNotInstalled": "{{distro}} 中未安裝 OpenCode", - "desktop.wsl.error.updateVersion": "OpenCode 更新完成,但 {{distro}} 回報的版本仍為 {{installed}};預期版本為 {{expected}}", - "desktop.wsl.error.noVersion": "無版本資訊", - "desktop.wsl.error.serverExited": "WSL 伺服器在啟動後結束 (code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": "WSL 伺服器在健全狀態確認前結束 (code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}} 的 Sidecar 健全狀態檢查在 {{timeout}}ms 後逾時", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 在 {{timeout}}ms 後逾時", - "desktop.wsl.error.failedPort": "無法取得連接埠", - "desktop.picker.error.notSelected": "檔案選擇器未選取檔案", - "desktop.picker.error.sizeLimit": "所選附件超過 {{limit}} MB 的限制", - "command.logs.export": "匯出記錄檔", - "command.session.export": "匯出工作階段", - "command.session.export.description": "將完整的工作階段記錄匯出為 JSON", - "provider.connect.method.browser": "瀏覽器", - "provider.connect.method.headless": "無頭模式", - "provider.custom.unavailable": "此伺服器無法使用自訂提供者", - "help.tabs.toast.ariaLabel": "分頁功能介紹。使用分頁整理工作和進行中的工作階段", - "help.tabs.toast.dismiss": "關閉分頁功能資訊", - "help.tabs.title": "分頁功能介紹", - "help.tabs.description": "使用分頁整理工作和進行中的工作階段", - "help.tabs.date": "7 月 14 日", - "help.tabs.introduction": "OpenCode Desktop 現在以分頁為核心。", - "help.tabs.sessions": "在分頁中開始新的工作階段,或從任一專案開啟現有的工作階段。開始新工作時請開啟新分頁,完成後再將其關閉。", - "help.tabs.organize": "同時開啟幾個分頁,可讓進行中的工作階段更容易整理。如果打算保留分頁,請將其重新命名為容易記住的名稱。", - "help.tabs.home": "你可以在新的首頁找到所有工作階段和專案。選取工作階段即可在分頁中開啟。", - "help.tabs.persistence": "重新開啟應用程式時,分頁仍會保持開啟。", - "help.tabs.worktrees": "新版設計尚未支援 Git Worktrees,此功能即將推出。如果你偏好繼續使用先前的版面,可以在設定中切換版面。請留意,新版面將在幾週後成為永久版面。", - "dialog.usageExceeded.dontShowAgain": "不再顯示", - "context.export.session": "匯出工作階段", - "toast.session.export.success.title": "工作階段已匯出", - "toast.session.export.success.description": "已將工作階段儲存至 {{filename}}", - "toast.session.export.failed.title": "匯出工作階段失敗", - "toast.session.export.failed.description": "匯出工作階段時發生錯誤", - "terminal.connectTicket.csrfError": "PTY 連線票證因來源或 CSRF 檢查而遭拒。請檢查伺服器 CORS 設定。", - "terminal.connectTicket.statusError": "PTY 連線票證失敗,狀態為 {{status}}", - "titlebar.update": "更新", - "titlebar.updateVersion": "更新 {{version}}", - "common.export": "匯出", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "強制所有互動元素顯示焦點樣式", - "debugBar.focus.on": "開啟", - "debugBar.focus.off": "關閉", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "在不變更所選語言的情況下,強制設定整個應用程式的版面配置方向", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - // English fallback — Amicode key with no locale translation yet (#796). - "session.exportTrace": "Export trace", } diff --git a/packages/app-bundle/overlay/packages/app/src/pages/layout/helpers.ts b/packages/app-bundle/overlay/packages/app/src/pages/layout/helpers.ts index 4d96514d..4105a082 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/layout/helpers.ts +++ b/packages/app-bundle/overlay/packages/app/src/pages/layout/helpers.ts @@ -9,14 +9,6 @@ type SessionStore = { path: { directory: string } } -export function compareSessionTime(a: Session, b: Session) { - const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created) - if (updated !== 0) return updated - return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 -} - -// amicode: recency-window sort — sessions touched in the last minute sort by -// creation order (a live session's bubbling), older ones by last update. function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 return (a: Session, b: Session) => { @@ -42,28 +34,6 @@ export const sortedRootSessions = (store: SessionStore, now: number) => roots(st export const latestRootSession = (stores: SessionStore[], now: number) => stores.flatMap(roots).sort(sortSessions(now))[0] -/** - * Directories whose sessions the list surfaces (Sessions dropdown, home list) - * aggregate over: the user's opened projects, falling back to the server's - * registered projects when nothing has been opened yet (fresh client, empty - * persisted registry — amicode#288). Session listing only; this must NOT feed - * the project switcher, or closing a server-registered project becomes - * impossible (the fallback would re-add it). - */ -export function sessionListDirectories( - opened: { worktree: string; sandboxes?: string[] }[], - serverProjects: { worktree: string; sandboxes?: string[] }[], -): string[] { - const dirs = opened.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]) - if (dirs.length > 0) return dirs - const seen = new Set() - return serverProjects.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]).filter((d) => { - if (!d || seen.has(d)) return false - seen.add(d) - return true - }) -} - export function hasProjectPermissions( request: Record | undefined, include: (item: T) => boolean = () => true, @@ -87,6 +57,28 @@ export const childSessionOnPath = (sessions: Session[] | undefined, rootID: stri export const displayName = (project: { name?: string; worktree: string }) => project.name || getFilename(project.worktree) || project.worktree +/** + * Directories whose sessions the list surfaces (Sessions dropdown, home list) + * aggregate over: the user's opened projects, falling back to the server's + * registered projects when nothing has been opened yet (fresh client, empty + * persisted registry — amicode#288). Session listing only; this must NOT feed + * the project switcher, or closing a server-registered project becomes + * impossible (the fallback would re-add it). + */ +export function sessionListDirectories( + opened: { worktree: string; sandboxes?: string[] }[], + serverProjects: { worktree: string; sandboxes?: string[] }[], +): string[] { + const dirs = opened.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]) + if (dirs.length > 0) return dirs + const seen = new Set() + return serverProjects.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]).filter((d) => { + if (!d || seen.has(d)) return false + seen.add(d) + return true + }) +} + export function toggleHomeProjectSelection( current: HomeProjectSelection | undefined, server: ServerConnection.Key, diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session.tsx index 3334c753..f276dc1a 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session.tsx +++ b/packages/app-bundle/overlay/packages/app/src/pages/session.tsx @@ -88,7 +88,7 @@ import { sessionPanelLayout } from "@/pages/session/session-panel-layout" import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2" import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2" import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" -import { accumulateDiffs } from "@/pages/session/v2/accumulate-diffs" +import { accumulateDiffs, applyRenames, mergeServerAndToolDiffs, toHomePath } from "@/pages/session/v2/accumulate-diffs" import { TerminalPanel } from "@/pages/session/terminal-panel" import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2" import { useComposerCommands } from "@/pages/session/use-composer-commands" @@ -681,6 +681,47 @@ export default function Page() { const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]) + + // --- File rename tracking (sidebar move/rename → Files Changed update) --- + // When the sidebar moves or renames a file, the extension posts a + // file-op-notify message. We track the rename so the tool-metadata diffs + // (which still have the old path) are displayed at the new location. + const [fileRenames, setFileRenames] = createSignal(new Map()) + const onFileOpNotify = (e: MessageEvent) => { + const d = e.data as { source?: string; kind?: string; op?: string; oldPath?: string; newPath?: string; home?: string } | undefined + if (d?.source !== "amicode" || d?.kind !== "file-op-notify") return + if ((d.op === "move" || d.op === "rename") && d.oldPath && d.newPath) { + // Use the SAME home as the reviewDiffs memo (globalThis.process.env.HOME, + // which is undefined in the browser). This ensures rename map keys match + // tool-diff paths — both end up as absolute paths in the browser iframe. + // Do NOT use d.home here: that produces ~/... keys while the memo produces + // /Users/... paths, and they never match. + const dir = sdk().directory + const home = typeof globalThis.process !== "undefined" ? globalThis.process.env?.HOME : undefined + const prefix = home && dir.startsWith(home) ? "~" + dir.slice(home.length) : dir + const oldNorm = toHomePath(d.oldPath, home, prefix) + const newNorm = toHomePath(d.newPath, home, prefix) + setFileRenames((prev) => { + const next = new Map(prev) + // Chain resolution: if anything pointed to oldPath, update it to newPath + for (const [k, v] of next) { + if (v === oldNorm) next.set(k, newNorm) + } + next.set(oldNorm, newNorm) + return next + }) + // Force a server refetch so in-project files at their new location + // appear immediately — without this, the stale server response (which + // doesn't include the file) persists until the file watcher's 1s debounce. + const sessionID = params.id + if (sessionID) { + sync().set("diff_version", sessionID, (v: number | undefined) => (v ?? 0) + 1) + } + } + } + window.addEventListener("message", onFileOpNotify) + onCleanup(() => window.removeEventListener("message", onFileOpNotify)) + // Refetch when the session transitions to idle (assistant finished, snapshot taken) // or when a file-editing tool completes mid-turn (diff_version bumps) const sessionDiffVersion = () => { @@ -706,35 +747,16 @@ export default function Page() { } }) const reviewDiffs = createMemo(() => { - // Server endpoint returns the authoritative full-session diff (queries all messages). - const serverDiffs = sessionDiffQuery.data ?? [] - // Once the server has responded at least once, trust it — even if it returned []. - // The client-side fallback is only for the initial load before any server response, - // never during refetches (where keepPreviousData already preserves the last result). - const serverResponded = sessionDiffQuery.status === "success" || sessionDiffQuery.isPlaceholderData - if (serverDiffs.length > 0 || serverResponded) { - // Server paths are relative to the project root — prefix with ~/project-path - const dir = sdk().directory - const home = typeof globalThis.process !== "undefined" ? globalThis.process.env?.HOME : undefined - const prefix = home && dir.startsWith(home) ? "~" + dir.slice(home.length) : dir - return serverDiffs - .filter((d): d is SnapshotFileDiff & { file: string } => !!d.file) - .map((d) => ({ ...d, file: d.file.startsWith("/") || d.file.startsWith("~/") ? d.file : `${prefix}/${d.file}` })) - } - // Fallback: derive from tool parts currently loaded in the client. - // This shows immediate results for visible messages while the server query loads. - const allMessages = messages() - if (!allMessages.length) return [] as Array + // Shared path normalization — both sources must use the same function for dedup to work. const dir = sdk().directory const home = typeof globalThis.process !== "undefined" ? globalThis.process.env?.HOME : undefined const prefix = home && dir.startsWith(home) ? "~" + dir.slice(home.length) : dir - const toHomePath = (p: string) => { - if (p.startsWith("~/")) return p - if (home && p.startsWith(home)) return "~" + p.slice(home.length) - if (!p.startsWith("/")) return `${prefix}/${p}` - return p - } + + // --- Tool-metadata diffs (always computed, not just as fallback) --- + // Scraped from completed edit/write/patch/apply_patch tool parts across all messages. + // These see every file the agent touched regardless of directory. const editParts: import("@/pages/session/v2/accumulate-diffs").ToolEditPart[] = [] + const allMessages = messages() for (const msg of allMessages) { const msgParts = sync().data.part[msg.id] if (!msgParts) continue @@ -747,7 +769,7 @@ export default function Page() { const rawTitle = (part.state as { title?: string }).title || filediff.file editParts.push({ file: filediff.file, - title: toHomePath(rawTitle), + title: toHomePath(rawTitle, home, prefix), patch: filediff.patch, additions: filediff.additions ?? 0, deletions: filediff.deletions ?? 0, @@ -755,7 +777,19 @@ export default function Page() { } } } - return accumulateDiffs(editParts) + const toolDiffs = applyRenames(accumulateDiffs(editParts), fileRenames()) + + // --- Server diffs (authoritative for in-project files) --- + const serverDiffs = sessionDiffQuery.data ?? [] + const serverResponded = sessionDiffQuery.status === "success" || sessionDiffQuery.isPlaceholderData + + return mergeServerAndToolDiffs({ + serverDiffs, + toolDiffs, + serverResponded, + directory: dir, + home, + }) }) // All files touched by edit tools in this session — fetched from the server @@ -1318,7 +1352,14 @@ export default function Page() { return layout.review.diffStyle() }, onDiffStyleChange: layout.review.setDiffStyle, + get serverUrl() { + return serverSDK().url + }, state: reviewV2State, + get isAgentBusy() { + const id = params.id + return id ? sync().data.session_working(id) : false + }, onRefresh: () => { const id = params.id if (id) sync().set("diff_version", id, (v: number | undefined) => (v ?? 0) + 1) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts index cec26586..667a9a85 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { accumulateDiffs, type ToolEditPart } from "./accumulate-diffs" +import { accumulateDiffs, applyRenames, mergeServerAndToolDiffs, type ToolEditPart } from "./accumulate-diffs" const edit = (file: string, overrides: Partial = {}): ToolEditPart => ({ file, @@ -112,3 +112,259 @@ describe("accumulateDiffs", () => { // on the query so the fallback never fires while server data exists. }) }) + +// --- mergeServerAndToolDiffs --- + +const HOME = "/Users/jj" +const DIR = "/Users/jj/harmoniqs/amicode" +const diff = ( + file: string, + patch = "mock", + additions = 1, + deletions = 0, + status: "added" | "modified" | "deleted" = "modified", +) => ({ file, patch, additions, deletions, status }) + +describe("mergeServerAndToolDiffs", () => { + test("cross-project tool diffs pass through when server has responded", () => { + const result = mergeServerAndToolDiffs({ + serverDiffs: [diff("src/foo.ts")], + toolDiffs: [diff("~/other-project/bar.ts")], + serverResponded: true, + directory: DIR, + home: HOME, + }) + const files = result.map((d) => d.file) + expect(files).toContain("~/harmoniqs/amicode/src/foo.ts") + expect(files).toContain("~/other-project/bar.ts") + }) + + test("in-project files come from the server, not tool metadata (server wins)", () => { + const result = mergeServerAndToolDiffs({ + serverDiffs: [diff("src/foo.ts", "server-patch", 10, 5)], + toolDiffs: [diff("~/harmoniqs/amicode/src/foo.ts", "tool-patch", 3, 1)], + serverResponded: true, + directory: DIR, + home: HOME, + }) + const entry = result.find((d) => d.file.endsWith("src/foo.ts")) + expect(entry).toBeDefined() + expect(entry!.patch).toBe("server-patch") + expect(entry!.additions).toBe(10) + }) + + test("in-project file absent from server (created+deleted) is excluded, not leaked as cross-project", () => { + // The bug: file was created by the write tool, then manually deleted. + // Server correctly has no diff (net zero). Tool metadata still has it. + // The old code leaked it through the cross-project filter. + const result = mergeServerAndToolDiffs({ + serverDiffs: [], + toolDiffs: [diff("~/harmoniqs/amicode/files-changed-test.md", "stale-patch", 3, 0, "added")], + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(result).toHaveLength(0) + }) + + test("in-project file with relative path absent from server is also excluded", () => { + const result = mergeServerAndToolDiffs({ + serverDiffs: [], + toolDiffs: [diff("~/harmoniqs/amicode/src/temp.ts")], + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(result).toHaveLength(0) + }) + + test("fallback: all tool diffs shown when server has not responded", () => { + const result = mergeServerAndToolDiffs({ + serverDiffs: [], + toolDiffs: [ + diff("~/harmoniqs/amicode/src/foo.ts"), + diff("~/other/bar.ts"), + ], + serverResponded: false, + directory: DIR, + home: HOME, + }) + expect(result).toHaveLength(2) + }) + + test("dedup by normalized path — server wins on conflict", () => { + const result = mergeServerAndToolDiffs({ + serverDiffs: [diff("/Users/jj/harmoniqs/amicode/src/foo.ts")], + toolDiffs: [diff("~/harmoniqs/amicode/src/foo.ts")], + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(result).toHaveLength(1) + }) +}) + +// --- applyRenames --- + +describe("applyRenames", () => { + test("renames a file path when it appears in the rename map", () => { + const diffs = [diff("~/harmoniqs/amicode/test.md")] + const renames = new Map([["~/harmoniqs/amicode/test.md", "~/harmoniqs/opencode/test.md"]]) + const result = applyRenames(diffs, renames) + expect(result).toHaveLength(1) + expect(result[0].file).toBe("~/harmoniqs/opencode/test.md") + }) + + test("leaves files unchanged when not in the rename map", () => { + const diffs = [diff("~/harmoniqs/amicode/src/foo.ts")] + const renames = new Map([["~/harmoniqs/amicode/test.md", "~/harmoniqs/opencode/test.md"]]) + const result = applyRenames(diffs, renames) + expect(result[0].file).toBe("~/harmoniqs/amicode/src/foo.ts") + }) + + test("returns diffs unchanged when rename map is empty", () => { + const diffs = [diff("~/harmoniqs/amicode/test.md"), diff("~/other/bar.ts")] + const result = applyRenames(diffs, new Map()) + expect(result).toEqual(diffs) + }) + + test("preserves all other diff fields (patch, additions, deletions, status)", () => { + const diffs = [diff("~/old/path.ts", "the-patch", 5, 3, "modified")] + const renames = new Map([["~/old/path.ts", "~/new/path.ts"]]) + const result = applyRenames(diffs, renames) + expect(result[0].file).toBe("~/new/path.ts") + expect(result[0].patch).toBe("the-patch") + expect(result[0].additions).toBe(5) + expect(result[0].deletions).toBe(3) + expect(result[0].status).toBe("modified") + }) + + test("handles multiple renames in one pass", () => { + const diffs = [diff("~/a/one.ts"), diff("~/b/two.ts")] + const renames = new Map([ + ["~/a/one.ts", "~/c/one.ts"], + ["~/b/two.ts", "~/d/two.ts"], + ]) + const result = applyRenames(diffs, renames) + expect(result[0].file).toBe("~/c/one.ts") + expect(result[1].file).toBe("~/d/two.ts") + }) +}) + +// --- move-back-to-project: full round-trip scenario --- + +describe("round-trip move: project → cross-project → back to project", () => { + test("file moved out then back is visible when server includes it after refetch", () => { + // Step 1: file created in-project, server has it + const step1 = mergeServerAndToolDiffs({ + serverDiffs: [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + toolDiffs: [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(step1).toHaveLength(1) + expect(step1[0].file).toBe("~/harmoniqs/amicode/test.md") + + // Step 2: file moved to opencode — rename map transforms tool diff + const renames1 = new Map([["~/harmoniqs/amicode/test.md", "~/harmoniqs/opencode/test.md"]]) + const toolDiffsStep2 = applyRenames( + [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + renames1, + ) + const step2 = mergeServerAndToolDiffs({ + serverDiffs: [], // server no longer has it (moved away) + toolDiffs: toolDiffsStep2, + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(step2).toHaveLength(1) + expect(step2[0].file).toBe("~/harmoniqs/opencode/test.md") + + // Step 3: file moved back — rename chain resolves to original path + // After chain resolution: amicode/test.md → amicode/test.md (identity) + const renames2 = new Map([ + ["~/harmoniqs/amicode/test.md", "~/harmoniqs/amicode/test.md"], + ["~/harmoniqs/opencode/test.md", "~/harmoniqs/amicode/test.md"], + ]) + const toolDiffsStep3 = applyRenames( + [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + renames2, + ) + // Tool diff is back at the in-project path + expect(toolDiffsStep3[0].file).toBe("~/harmoniqs/amicode/test.md") + + // With server refetch (diff_version bumped), server includes the file again + const step3 = mergeServerAndToolDiffs({ + serverDiffs: [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + toolDiffs: toolDiffsStep3, + serverResponded: true, + directory: DIR, + home: HOME, + }) + expect(step3).toHaveLength(1) + expect(step3[0].file).toBe("~/harmoniqs/amicode/test.md") + }) + + test("file moved back DISAPPEARS when server has NOT refetched (the bug this fix addresses)", () => { + // Rename map resolves to in-project, but server still has stale empty response + const renames = new Map([ + ["~/harmoniqs/amicode/test.md", "~/harmoniqs/amicode/test.md"], + ]) + const toolDiffs = applyRenames( + [diff("~/harmoniqs/amicode/test.md", "patch", 2, 0, "added")], + renames, + ) + // Server hasn't refetched — stale empty response + const result = mergeServerAndToolDiffs({ + serverDiffs: [], + toolDiffs, + serverResponded: true, + directory: DIR, + home: HOME, + }) + // Without the diff_version bump, the file is gone (server is authoritative for in-project) + expect(result).toHaveLength(0) + }) +}) + +// --------------------------------------------------------------------------- +// latchWhileBusy — prevent brief falsy gaps from unmounting components +// --------------------------------------------------------------------------- + +describe("latchWhileBusy", () => { + // Pure-function version of the latching memo used in ReviewPanelV2. + // The real code uses createMemo with a prev accumulator; we test the + // underlying logic here as a plain function. + function latchWhileBusy(current: T | undefined, prev: T | undefined, isBusy: boolean): T | undefined { + if (!current && prev && isBusy) return prev + return current + } + + test("returns current value when it is truthy", () => { + expect(latchWhileBusy("foo.ts", undefined, true)).toBe("foo.ts") + expect(latchWhileBusy("foo.ts", "bar.ts", true)).toBe("foo.ts") + expect(latchWhileBusy("foo.ts", undefined, false)).toBe("foo.ts") + }) + + test("latches previous value when current goes falsy and agent is busy", () => { + expect(latchWhileBusy(undefined, "foo.ts", true)).toBe("foo.ts") + }) + + test("does NOT latch when agent is idle (allows real empty state)", () => { + expect(latchWhileBusy(undefined, "foo.ts", false)).toBeUndefined() + }) + + test("does NOT latch when there is no previous value", () => { + expect(latchWhileBusy(undefined, undefined, true)).toBeUndefined() + }) + + test("latches object values the same way", () => { + const prevDiff = { file: "foo.ts", additions: 5, deletions: 2 } + expect(latchWhileBusy(undefined, prevDiff, true)).toBe(prevDiff) + expect(latchWhileBusy(undefined, prevDiff, false)).toBeUndefined() + const newDiff = { file: "foo.ts", additions: 6, deletions: 3 } + expect(latchWhileBusy(newDiff, prevDiff, true)).toBe(newDiff) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts index 8e069fb7..22d4e1ef 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts @@ -8,6 +8,61 @@ export type ToolEditPart = { deletions: number } +/** + * Normalize a file path to ~/... form for consistent dedup across sources. + */ +export function toHomePath(p: string, home: string | undefined, prefix: string): string { + if (p.startsWith("~/")) return p + if (home && p.startsWith(home)) return "~" + p.slice(home.length) + if (!p.startsWith("/")) return `${prefix}/${p}` + return p +} + +export interface MergeOpts { + serverDiffs: SnapshotFileDiff[] + toolDiffs: Array + serverResponded: boolean + directory: string + home: string | undefined +} + +/** + * Merge server shadow-git diffs with tool-metadata diffs. + * + * Server diffs are authoritative for in-project files. Tool-metadata diffs + * fill in cross-project files (outside the project directory) not covered + * by the server. In-project tool-metadata diffs that the server excluded + * (e.g. created + deleted = net zero) are dropped — the server's absence + * is the authority. + * + * When serverResponded is false (initial load), all tool-metadata diffs + * are returned as a fallback. + */ +export function mergeServerAndToolDiffs(opts: MergeOpts): Array { + const { serverDiffs, toolDiffs, serverResponded, directory, home } = opts + const prefix = home && directory.startsWith(home) ? "~" + directory.slice(home.length) : directory + const projectPrefix = prefix + "/" + + if (serverDiffs.length > 0 || serverResponded) { + const normalizedServerDiffs = serverDiffs + .filter((d): d is SnapshotFileDiff & { file: string } => !!d.file) + .map((d) => ({ ...d, file: toHomePath(d.file, home, prefix) })) + + const serverFiles = new Set(normalizedServerDiffs.map((d) => d.file)) + // Only pass through tool-metadata diffs that are BOTH absent from the + // server set AND outside the project directory. In-project files trust + // the server's authority — if the server excluded them (created + deleted, + // or reverted), they should not leak through as phantom entries. + const crossProjectDiffs = toolDiffs.filter( + (d) => !serverFiles.has(d.file) && !d.file.startsWith(projectPrefix), + ) + + return [...normalizedServerDiffs, ...crossProjectDiffs] + } + + return toolDiffs +} + /** * Accumulate file diffs from tool edit parts across a session. * @@ -62,3 +117,20 @@ export function accumulateDiffs(parts: ToolEditPart[]): Array, + renames: Map, +): Array { + if (renames.size === 0) return diffs + return diffs.map((d) => { + const newPath = renames.get(d.file) + return newPath ? { ...d, file: newPath } : d + }) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2.tsx index a10099bb..b865347a 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -7,7 +7,7 @@ import { SessionReviewV2, SessionReviewV2Sidebar, } from "@opencode-ai/session-ui/v2/session-review-v2" -import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2" +import { SessionReviewFilePreviewV2, type SessionReviewFilePreviewV2Props } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2" import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2" import { FileIcon } from "@opencode-ai/ui/file-icon" import type { @@ -45,7 +45,9 @@ export type ReviewPanelV2Props = { onSelectFile: (path: string) => void diffStyle: SessionReviewDiffStyle onDiffStyleChange?: (style: SessionReviewDiffStyle) => void + serverUrl?: string state: ReviewPanelV2State + isAgentBusy?: boolean onRefresh?: () => void onLineComment?: (comment: SessionReviewLineComment) => void onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void @@ -79,6 +81,12 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { if (searching()) return active const files = filteredFiles() if (active && files.includes(active)) return active + // During agent turns, keep the user's selection even when the file has + // briefly dropped from the diff list (the ~1s blind window between tool + // completion and server diff refetch). Without this, activeDiff falls + // through to files[0], the keyed sees a different key, and the + // preview component remounts — losing scroll position and editor state. + if (active && props.isAgentBusy) return active return files[0] }) const sourceActiveItem = createMemo(() => diffs().find((diff) => diff.file === activeDiff())) @@ -102,6 +110,22 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { return source }) + // Latch the active file and diff during agent turns so the wrappers + // don't unmount SessionReviewFilePreviewV2 during the ~1s blind window + // between a tool completion and the server diff refetch. Without this, + // the component is destroyed and recreated, losing scroll position and + // editor state every time the agent edits a file. + const stableActiveDiff = createMemo((prev) => { + const current = activeDiff() + if (!current && prev && props.isAgentBusy) return prev + return current + }) + const stableActiveItem = createMemo((prev) => { + const current = activeItem() + if (!current && prev && props.isAgentBusy) return prev + return current + }) + const readFile = async (path: string) => sdk() .client.file.read({ path }) @@ -132,7 +156,7 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { activeDiff={activeDiff} /> } - activeFile={activeDiff()} + activeFile={stableActiveDiff()} files={filteredFiles()} onSelectFile={props.onSelectFile} diffStyle={props.diffStyle} @@ -143,16 +167,21 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { preview={ // Key on the file path, not the diff object identity, so refreshed diff data // updates the mounted preview instead of remounting the whole viewer. - + // Use stable (latched) values so the component survives brief falsy + // gaps during the agent's turn (the ~1s blind window between tool + // completion and server diff refetch). + {(file) => ( - + {(diff) => ( { const files = filteredFiles() diff --git a/packages/app-bundle/overlay/packages/session-ui/package.json b/packages/app-bundle/overlay/packages/session-ui/package.json index 04a4f158..6672f469 100644 --- a/packages/app-bundle/overlay/packages/session-ui/package.json +++ b/packages/app-bundle/overlay/packages/session-ui/package.json @@ -41,7 +41,18 @@ "vite": "catalog:" }, "dependencies": { + "@codemirror/lang-css": "6.3.1", + "@codemirror/lang-html": "6.4.12", + "@codemirror/lang-javascript": "6.2.5", + "@codemirror/lang-json": "6.0.2", + "@codemirror/lang-markdown": "6.5.2", + "@codemirror/lang-python": "6.2.1", + "@codemirror/language": "6.12.4", + "@codemirror/merge": "6.12.2", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.9", "@kobalte/core": "catalog:", + "@lezer/highlight": "1.2.3", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx index a5e98f8d..4d932c80 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx @@ -1389,16 +1389,22 @@ export function ContextToolGroup(props: { props.actions?.openAttachment?.(file)} > - {typeLabel(name, file.mime, i18n.t("ui.common.file"))} + {typeLabel(name, file.mime)} ) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/diff-gutter-extension.ts b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/diff-gutter-extension.ts new file mode 100644 index 00000000..5a750cc1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/diff-gutter-extension.ts @@ -0,0 +1,414 @@ +/** + * Custom CM6 gutter extension for unified diff mode (#769). + * + * Diffs `original` vs the current editor document and renders: + * - Gutter markers: green (added), red (removed), yellow (changed) + * - Inline deleted-line widgets: faded red text showing removed content + * + * Updates on a debounced basis (~300ms) after each edit. + * Uses the `diff` npm package (already a session-ui dependency) for + * line-level diffing. + * + * @module + */ + +import { + type Extension, + StateField, + StateEffect, + RangeSet, + type EditorState, + type Range, +} from "@codemirror/state" +import { + EditorView, + GutterMarker, + gutter, + ViewPlugin, + Decoration, + WidgetType, + type ViewUpdate, + type DecorationSet, +} from "@codemirror/view" +import { diffLines, type Change } from "diff" + +// --------------------------------------------------------------------------- +// Marker kinds and GutterMarker subclasses +// --------------------------------------------------------------------------- + +export enum DiffGutterMarkerKind { + Added = "added", + Removed = "removed", + Changed = "changed", +} + +class DiffMarker extends GutterMarker { + constructor(readonly kind: DiffGutterMarkerKind) { + super() + } + + toDOM(): HTMLElement { + const el = document.createElement("div") + el.className = `cm-diff-gutter-marker cm-diff-gutter-${this.kind}` + el.style.width = "4px" + el.style.height = "100%" + el.style.borderRadius = "1px" + + switch (this.kind) { + case DiffGutterMarkerKind.Added: + el.style.backgroundColor = "var(--v2-state-fg-success, var(--text-on-success-base, #4caf50))" + break + case DiffGutterMarkerKind.Removed: + el.style.backgroundColor = "var(--v2-state-fg-danger, var(--text-on-critical-base, #f44336))" + break + case DiffGutterMarkerKind.Changed: + el.style.backgroundColor = "var(--v2-state-fg-warning, var(--border-warning-base, #ffc107))" + break + } + + return el + } +} + +const addedMarker = new DiffMarker(DiffGutterMarkerKind.Added) +const removedMarker = new DiffMarker(DiffGutterMarkerKind.Removed) +const changedMarker = new DiffMarker(DiffGutterMarkerKind.Changed) + +// --------------------------------------------------------------------------- +// Deleted-line widget — renders removed text as faded read-only lines +// --------------------------------------------------------------------------- + +class DeletedLinesWidget extends WidgetType { + constructor(readonly lines: string[]) { + super() + } + + toDOM(): HTMLElement { + const wrapper = document.createElement("div") + wrapper.className = "cm-deleted-line-widget" + wrapper.style.fontFamily = "var(--font-mono, ui-monospace, monospace)" + wrapper.style.fontSize = "13px" + wrapper.style.lineHeight = "1.5" + wrapper.style.pointerEvents = "none" + wrapper.style.userSelect = "none" + wrapper.setAttribute("aria-hidden", "true") + + for (const line of this.lines) { + const row = document.createElement("div") + row.className = "cm-deleted-line-row" + row.style.backgroundColor = "var(--v2-state-bg-danger, rgba(244, 67, 54, 0.06))" + row.style.color = "var(--v2-state-fg-danger, var(--text-on-critical-base, #f44336))" + row.style.opacity = "0.7" + row.style.textDecoration = "line-through" + row.style.padding = "0 4px" + row.style.whiteSpace = "pre-wrap" + row.style.wordBreak = "break-all" + // Prefix with minus sign like a diff + row.textContent = `- ${line}` + wrapper.appendChild(row) + } + + return wrapper + } + + eq(other: DeletedLinesWidget): boolean { + return ( + this.lines.length === other.lines.length && + this.lines.every((l, i) => l === other.lines[i]) + ) + } + + get estimatedHeight(): number { + // ~20px per line (13px font * 1.5 line-height) + return this.lines.length * 20 + } + + ignoreEvent(): boolean { + return true + } +} + +// --------------------------------------------------------------------------- +// State effects and fields +// --------------------------------------------------------------------------- + +const setDiffMarkers = StateEffect.define>() +const setDeletedDecorations = StateEffect.define() + +const diffMarkersField = StateField.define>({ + create() { + return RangeSet.empty + }, + update(markers, tr) { + for (const effect of tr.effects) { + if (effect.is(setDiffMarkers)) { + return effect.value + } + } + return markers + }, +}) + +const deletedDecorationsField = StateField.define({ + create() { + return Decoration.none + }, + update(decos, tr) { + for (const effect of tr.effects) { + if (effect.is(setDeletedDecorations)) { + return effect.value + } + } + // Map through document changes so positions stay valid + return decos.map(tr.changes) + }, + provide: (f) => EditorView.decorations.from(f), +}) + +// --------------------------------------------------------------------------- +// Diff computation +// --------------------------------------------------------------------------- + +interface LineMarker { + line: number + kind: DiffGutterMarkerKind +} + +/** Where to insert a deleted-lines widget: after `line` in the current doc. */ +interface DeletedBlock { + afterLine: number + lines: string[] +} + +interface DiffResult { + markers: LineMarker[] + deletedBlocks: DeletedBlock[] +} + +function computeDiff(original: string, current: string): DiffResult { + const changes = diffLines(original, current) + const markers: LineMarker[] = [] + const deletedBlocks: DeletedBlock[] = [] + + let currentLine = 1 + + for (let i = 0; i < changes.length; i++) { + const change = changes[i] + const lineCount = change.count ?? 0 + + if (change.removed) { + const removedText = (change.value ?? "").replace(/\n$/, "") + const removedLines = removedText.split("\n") + + // Check if the next change is an addition (replacement) + if (i + 1 < changes.length && changes[i + 1].added) { + const nextChange = changes[i + 1] + const nextCount = nextChange.count ?? 0 + // This is a replacement — mark added lines as "changed" + for (let j = 0; j < nextCount; j++) { + markers.push({ + line: currentLine + j, + kind: DiffGutterMarkerKind.Changed, + }) + } + // Show the deleted lines above the changed lines + deletedBlocks.push({ + afterLine: Math.max(0, currentLine - 1), + lines: removedLines, + }) + currentLine += nextCount + i++ // Skip the next (added) change since we handled it + } else { + // Pure removal — show deleted lines at the current position + markers.push({ + line: Math.max(1, currentLine), + kind: DiffGutterMarkerKind.Removed, + }) + deletedBlocks.push({ + afterLine: Math.max(0, currentLine - 1), + lines: removedLines, + }) + } + // Removed-only: currentLine doesn't advance + } else if (change.added) { + // Pure addition (not a replacement — those are handled above) + for (let j = 0; j < lineCount; j++) { + markers.push({ + line: currentLine + j, + kind: DiffGutterMarkerKind.Added, + }) + } + currentLine += lineCount + } else { + // Unchanged lines + currentLine += lineCount + } + } + + return { markers, deletedBlocks } +} + +function markersToRangeSet( + markers: LineMarker[], + state: EditorState, +): RangeSet { + const builder: { from: number; marker: DiffMarker }[] = [] + + for (const m of markers) { + if (m.line < 1 || m.line > state.doc.lines) continue + + const lineStart = state.doc.line(m.line).from + const marker = + m.kind === DiffGutterMarkerKind.Added + ? addedMarker + : m.kind === DiffGutterMarkerKind.Removed + ? removedMarker + : changedMarker + + builder.push({ from: lineStart, marker }) + } + + builder.sort((a, b) => a.from - b.from) + + return RangeSet.of( + builder.map((b) => b.marker.range(b.from)), + ) +} + +function deletedBlocksToDecorations( + blocks: DeletedBlock[], + state: EditorState, +): DecorationSet { + const widgets: Range[] = [] + + for (const block of blocks) { + if (block.lines.length === 0) continue + + // Position: after the specified line, or at doc start if afterLine is 0 + let pos: number + if (block.afterLine <= 0) { + pos = 0 + } else if (block.afterLine >= state.doc.lines) { + pos = state.doc.length + } else { + pos = state.doc.line(block.afterLine).to + } + + const widget = Decoration.widget({ + widget: new DeletedLinesWidget(block.lines), + block: true, + side: 1, // After the line + }) + + widgets.push(widget.range(pos)) + } + + // Sort by position (required by RangeSet) + widgets.sort((a, b) => a.from - b.from) + + return Decoration.set(widgets) +} + +// --------------------------------------------------------------------------- +// View plugin: debounced diff recomputation +// --------------------------------------------------------------------------- + +const DEBOUNCE_MS = 300 + +function createDiffPlugin(original: string) { + return ViewPlugin.fromClass( + class { + private timer: ReturnType | null = null + private original: string + + constructor(private view: EditorView) { + this.original = original + // Defer initial diff computation — can't dispatch during construction + this.timer = setTimeout(() => { + this.timer = null + this.computeAndApply() + }, 0) + } + + update(update: ViewUpdate) { + if (update.docChanged) { + this.scheduleCompute() + } + } + + private scheduleCompute() { + if (this.timer !== null) { + clearTimeout(this.timer) + } + this.timer = setTimeout(() => { + this.timer = null + this.computeAndApply() + }, DEBOUNCE_MS) + } + + private computeAndApply() { + const current = this.view.state.doc.toString() + const { markers, deletedBlocks } = computeDiff(this.original, current) + const markerSet = markersToRangeSet(markers, this.view.state) + const decoSet = deletedBlocksToDecorations(deletedBlocks, this.view.state) + this.view.dispatch({ + effects: [ + setDiffMarkers.of(markerSet), + setDeletedDecorations.of(decoSet), + ], + }) + } + + destroy() { + if (this.timer !== null) { + clearTimeout(this.timer) + } + } + }, + ) +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Create a CM6 extension that shows diff gutter markers + inline + * deleted-line widgets comparing the editor content against `original`. + * Markers and widgets update 300ms after the last edit. + */ +export function diffGutterExtension(original: string): Extension { + return [ + diffMarkersField, + deletedDecorationsField, + gutter({ + class: "cm-diff-gutter", + markers: (view) => view.state.field(diffMarkersField), + }), + createDiffPlugin(original), + // Gutter styling + EditorView.baseTheme({ + ".cm-diff-gutter": { + width: "6px", + minWidth: "6px", + marginRight: "2px", + }, + }), + ] +} + +/** + * Read the current diff markers from an EditorState. + * Useful for testing — avoids relying on DOM rendering. + */ +export function getDiffMarkers(state: EditorState): RangeSet { + return state.field(diffMarkersField) +} + +/** + * Read the current deleted-line decorations from an EditorState. + * Useful for testing. + */ +export function getDeletedLineDecorations(state: EditorState): DecorationSet { + return state.field(deletedDecorationsField) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view-core.ts b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view-core.ts new file mode 100644 index 00000000..1d9a71ed --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view-core.ts @@ -0,0 +1,545 @@ +/** + * EditableDiffView — CodeMirror 6 core logic. + * + * Pure logic layer: language loading, theme building, editor construction. + * The SolidJS component wrapper lives in editable-diff-view.tsx and uses + * these functions. Tests import this file directly (no JSX transform needed). + * + * @module + */ + +import { Compartment, EditorState, Transaction, ChangeSet, type Extension } from "@codemirror/state" +import { + EditorView, + lineNumbers, + drawSelection, + highlightActiveLine, + highlightSpecialChars, +} from "@codemirror/view" +import { + MergeView, + unifiedMergeView, + originalDocChangeEffect, + getOriginalDoc, +} from "@codemirror/merge" +import { type LanguageSupport } from "@codemirror/language" +import { + HighlightStyle, + syntaxHighlighting, +} from "@codemirror/language" +import { tags } from "@lezer/highlight" + +// --------------------------------------------------------------------------- +// Language loader — dynamic imports so unused grammars stay out of the bundle. +// --------------------------------------------------------------------------- + +const EXTENSION_MAP: Record Promise> = { + js: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: false, typescript: false })), + jsx: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: true, typescript: false })), + ts: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: false, typescript: true })), + tsx: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: true, typescript: true })), + mjs: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: false, typescript: false })), + cjs: () => import("@codemirror/lang-javascript").then((m) => m.javascript({ jsx: false, typescript: false })), + py: () => import("@codemirror/lang-python").then((m) => m.python()), + python: () => import("@codemirror/lang-python").then((m) => m.python()), + json: () => import("@codemirror/lang-json").then((m) => m.json()), + jsonc: () => import("@codemirror/lang-json").then((m) => m.json()), + md: () => import("@codemirror/lang-markdown").then((m) => m.markdown()), + markdown: () => import("@codemirror/lang-markdown").then((m) => m.markdown()), + css: () => import("@codemirror/lang-css").then((m) => m.css()), + scss: () => import("@codemirror/lang-css").then((m) => m.css()), + html: () => import("@codemirror/lang-html").then((m) => m.html()), + htm: () => import("@codemirror/lang-html").then((m) => m.html()), + xml: () => import("@codemirror/lang-html").then((m) => m.html()), + svg: () => import("@codemirror/lang-html").then((m) => m.html()), +} + +/** + * Dynamically load a CodeMirror language support by file extension. + * Returns null for unknown extensions. + */ +export async function loadLanguage( + ext: string, +): Promise { + const normalized = ext.replace(/^\./, "").toLowerCase() + const loader = EXTENSION_MAP[normalized] + if (!loader) return null + try { + return await loader() + } catch { + return null + } +} + +// --------------------------------------------------------------------------- +// Theme builder — maps app CSS custom properties to CM6 theme selectors. +// +// Uses the app's v2 design tokens (--v2-*) for structural chrome and the +// OpenCode Shiki token variables (--syntax-*) for syntax highlighting. Both +// systems switch light/dark purely via CSS — the CM6 theme is the same +// object in either mode, referencing variables that the app's theme resolver +// redefines for each color scheme. +// --------------------------------------------------------------------------- + +/** + * Build a CM6 theme extension using the app's design tokens. + * The `mode` parameter sets the CM6 `dark` flag (which controls default + * fallback colors); actual colors come from CSS custom properties. + */ +export function buildThemeExtension(mode: "light" | "dark"): Extension { + const isDark = mode === "dark" + + return EditorView.theme( + { + "&": { + backgroundColor: "var(--v2-background-bg-base, var(--background-base))", + color: "var(--v2-text-text-base, var(--text-strong))", + fontFamily: "var(--font-mono, ui-monospace, monospace)", + fontSize: "13px", + lineHeight: "1.5", + }, + ".cm-content": { + caretColor: "var(--v2-text-text-base, var(--text-strong))", + fontFamily: "var(--font-mono, ui-monospace, monospace)", + }, + ".cm-cursor, .cm-dropCursor": { + borderLeftColor: "var(--v2-text-text-base, var(--text-strong))", + }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": + { + backgroundColor: "var(--v2-background-bg-layer-03, var(--background-weak))", + }, + ".cm-panels": { + backgroundColor: "var(--v2-background-bg-base, var(--background-base))", + color: "var(--v2-text-text-base, var(--text-strong))", + }, + ".cm-gutters": { + backgroundColor: "var(--v2-background-bg-layer-01, var(--background-weak))", + color: "var(--v2-text-text-muted, var(--text-weak))", + borderRight: "1px solid var(--v2-border-border-base, var(--border-base))", + }, + ".cm-activeLineGutter": { + backgroundColor: "var(--v2-background-bg-layer-02, var(--background-weak))", + }, + ".cm-activeLine": { + backgroundColor: "var(--v2-background-bg-layer-01, var(--background-weak))", + }, + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", + color: "var(--v2-text-text-faint, var(--text-weaker))", + }, + // Diff highlights — softer backgrounds that read well in both modes. + // The v2 state-bg tokens are designed for small badges; for full-line + // backgrounds we mix them down to ~40% so they tint without obscuring text. + ".cm-changedLine": { + backgroundColor: "color-mix(in srgb, var(--v2-state-bg-warning, var(--surface-warning-weak)) 40%, transparent)", + }, + ".cm-changedText": { + backgroundColor: "color-mix(in srgb, var(--v2-state-bg-warning, var(--surface-warning-base)) 60%, transparent)", + }, + ".cm-insertedLine": { + backgroundColor: "color-mix(in srgb, var(--v2-state-bg-success, var(--surface-success-weak)) 40%, transparent)", + }, + ".cm-deletedLine": { + backgroundColor: "color-mix(in srgb, var(--v2-state-bg-danger, var(--surface-critical-weak)) 30%, transparent)", + }, + }, + { dark: isDark }, + ) +} + +// --------------------------------------------------------------------------- +// Syntax highlight style — maps CM6 lezer tags to the app's --syntax-* +// CSS variables so highlighting matches the user's selected theme. +// --------------------------------------------------------------------------- + +/** + * Build a HighlightStyle that uses the app's --syntax-* CSS variables. + * Returns a syntaxHighlighting extension. + */ +export function buildSyntaxHighlightStyle(): Extension { + const style = HighlightStyle.define([ + { tag: [tags.comment, tags.lineComment, tags.blockComment], + color: "var(--syntax-comment, var(--text-weak))" }, + { tag: [tags.keyword, tags.controlKeyword, tags.operatorKeyword, tags.moduleKeyword], + color: "var(--syntax-keyword, var(--text-weak))" }, + { tag: [tags.string, tags.special(tags.string), tags.character], + color: "var(--syntax-string)" }, + { tag: [tags.number, tags.bool, tags.null], + color: "var(--syntax-primitive)" }, + { tag: [tags.typeName, tags.className, tags.namespace], + color: "var(--syntax-type)" }, + { tag: [tags.propertyName, tags.attributeName, tags.labelName], + color: "var(--syntax-property)" }, + { tag: [tags.variableName, tags.definition(tags.variableName)], + color: "var(--v2-text-text-base, var(--text-strong))" }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], + color: "var(--syntax-function, var(--syntax-property))" }, + { tag: [tags.constant(tags.variableName), tags.atom], + color: "var(--syntax-constant)" }, + { tag: [tags.operator, tags.punctuation, tags.separator], + color: "var(--v2-text-text-muted, var(--text-base))" }, + { tag: [tags.meta, tags.annotation, tags.processingInstruction], + color: "var(--syntax-comment, var(--text-weak))" }, + { tag: tags.heading, + color: "var(--syntax-keyword, var(--text-strong))", fontWeight: "bold" }, + { tag: tags.emphasis, + fontStyle: "italic" }, + { tag: tags.strong, + fontWeight: "bold" }, + { tag: tags.link, + color: "var(--syntax-string)", textDecoration: "underline" }, + { tag: tags.invalid, + color: "var(--v2-state-fg-danger, var(--text-on-critical-base))" }, + ]) + + return syntaxHighlighting(style) +} + +// --------------------------------------------------------------------------- +// Shared extensions — base setup shared by all editor instances. +// --------------------------------------------------------------------------- + +/** + * Build the mutable extensions that live inside a Compartment and can be + * swapped via `setReadOnly()` without re-creating the editor. + */ +export function editableExtensions(opts: { + readOnly: boolean + onChange?: (content: string) => void +}): Extension[] { + const exts: Extension[] = [ + EditorView.editable.of(!opts.readOnly), + EditorState.readOnly.of(opts.readOnly), + ] + + if (opts.onChange && !opts.readOnly) { + exts.push( + EditorView.updateListener.of((update) => { + if (update.docChanged) { + opts.onChange!(update.state.doc.toString()) + } + }), + ) + } + + return exts +} + +export function baseExtensions(opts: { + theme: Extension + language?: LanguageSupport | null +}): Extension[] { + return [ + lineNumbers(), + highlightActiveLine(), + highlightSpecialChars(), + drawSelection(), + EditorView.lineWrapping, + buildSyntaxHighlightStyle(), + opts.theme, + ...(opts.language ? [opts.language] : []), + ] +} + +// --------------------------------------------------------------------------- +// Editor construction — imperative helpers for tests and the SolidJS wrapper. +// --------------------------------------------------------------------------- + +export interface DiffEditorHandle { + /** The active EditorView for the modified (editable) pane. */ + editorView: EditorView | null + /** The MergeView instance (split mode only). */ + mergeView: MergeView | null + /** The scrollable DOM element (split: mergeView.dom, unified: editorView.scrollDOM). */ + scrollDOM: HTMLElement | null + /** Destroy all editor instances. */ + destroy: () => void + /** Revert to original: replace content, clear undo history. */ + revert: (original: string) => void + /** Get the current document content. */ + getContent: () => string + /** + * Update the original (left/before) document in place via CM6 dispatch. + * No editor re-creation. No-op if text is unchanged. + */ + updateOriginal: (text: string) => void + /** + * Update the modified (right/after) document in place via CM6 dispatch. + * No editor re-creation. No-op if text is unchanged. + */ + updateModified: (text: string) => void + /** + * Toggle readOnly state in place via Compartment reconfigure. + * No editor re-creation, no scroll displacement. + * Pass `onChange` when switching to editable to re-attach the listener. + */ + setReadOnly: (readOnly: boolean, onChange?: (content: string) => void) => void +} + +export function createDiffEditor(opts: { + parent: HTMLElement + original: string + modified: string + diffStyle: "unified" | "split" + readOnly: boolean + theme: Extension + language?: LanguageSupport | null + onChange?: (content: string) => void +}): DiffEditorHandle { + let mergeView: MergeView | null = null + let editorView: EditorView | null = null + + // Compartment for the mutable readOnly/onChange extensions on the + // modifiable pane. Reconfigured in-place by setReadOnly() — no editor + // teardown, no scroll displacement. + const editableCompartment = new Compartment() + + const base = baseExtensions({ + theme: opts.theme, + language: opts.language, + }) + + if (opts.diffStyle === "split") { + mergeView = new MergeView({ + parent: opts.parent, + a: { + doc: opts.original, + extensions: [ + ...base, + // Original pane is always readOnly, no compartment needed + ...editableExtensions({ readOnly: true }), + ], + }, + b: { + doc: opts.modified, + extensions: [ + ...base, + editableCompartment.of( + editableExtensions({ + readOnly: opts.readOnly, + onChange: opts.readOnly ? undefined : opts.onChange, + }), + ), + ], + }, + }) + + // MergeView.dom is the PARENT of .cm-editor, so EditorView.theme() + // selectors (scoped under .cm-editor) can't target it. Set height + // directly so the MergeView fills its container and scrolls. + mergeView.dom.style.height = "100%" + mergeView.dom.style.overflow = "auto" + } else { + // Unified mode: @codemirror/merge's unifiedMergeView — the standard CM6 + // inline diff that interleaves deleted lines with the editable document. + const extensions: Extension[] = [ + ...base, + editableCompartment.of( + editableExtensions({ + readOnly: opts.readOnly, + onChange: opts.readOnly ? undefined : opts.onChange, + }), + ), + unifiedMergeView({ + original: EditorState.create({ doc: opts.original }).doc, + mergeControls: false, + gutter: true, + highlightChanges: true, + syntaxHighlightDeletions: true, + }), + ] + + editorView = new EditorView({ + state: EditorState.create({ + doc: opts.modified, + extensions, + }), + parent: opts.parent, + }) + + // Ensure the editor fills and scrolls within its container + editorView.dom.style.height = "100%" + } + + function getActiveView(): EditorView | null { + if (mergeView) return mergeView.b + return editorView + } + + return { + get editorView() { + return getActiveView() + }, + get mergeView() { + return mergeView + }, + get scrollDOM(): HTMLElement | null { + // Split mode: mergeView.dom is the scrollable container (overflow: auto) + if (mergeView) return mergeView.dom + // Unified mode: EditorView.scrollDOM is the CM6 scroll element + if (editorView) return editorView.scrollDOM + return null + }, + destroy() { + mergeView?.destroy() + editorView?.destroy() + mergeView = null + editorView = null + }, + revert(original: string) { + const view = getActiveView() + if (!view) return + + // Replace entire document with original + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: original, + }, + }) + }, + getContent() { + const view = getActiveView() + return view?.state.doc.toString() ?? "" + }, + updateOriginal(text: string) { + if (mergeView) { + // Split mode: dispatch minimal change to the A (original) pane + const aView = mergeView.a + const current = aView.state.doc.toString() + const change = minimalChanges(current, text) + if (!change) return + aView.dispatch({ + changes: change, + annotations: Transaction.addToHistory.of(false), + }) + } else if (editorView) { + // Unified mode: update via originalDocChangeEffect + const origDoc = getOriginalDoc(editorView.state) + const currentText = origDoc.toString() + if (currentText === text) return + const changes = ChangeSet.of( + { from: 0, to: origDoc.length, insert: text }, + origDoc.length, + ) + editorView.dispatch({ + effects: originalDocChangeEffect(editorView.state, changes), + annotations: Transaction.addToHistory.of(false), + }) + } + }, + updateModified(text: string) { + const view = getActiveView() + if (!view) return + const current = view.state.doc.toString() + const change = minimalChanges(current, text) + if (!change) return + view.dispatch({ + changes: change, + annotations: Transaction.addToHistory.of(false), + }) + }, + setReadOnly(readOnly: boolean, onChange?: (content: string) => void) { + const view = getActiveView() + if (!view) return + view.dispatch({ + effects: editableCompartment.reconfigure( + editableExtensions({ readOnly, onChange }), + ), + }) + }, + } +} + +/** + * Detect dark/light mode from DOM — reads `data-color-scheme` attribute + * on (set by the app's theme system). + */ +export function detectMode(): "light" | "dark" { + if (typeof document === "undefined") return "dark" + const html = document.documentElement + // The app sets data-color-scheme on + const scheme = html.getAttribute("data-color-scheme") + if (scheme === "light") return "light" + if (scheme === "dark") return "dark" + // Fallbacks for other conventions + if (html.classList.contains("dark")) return "dark" + if (html.getAttribute("data-theme") === "dark") return "dark" + if ( + typeof window !== "undefined" && + window.matchMedia?.("(prefers-color-scheme: dark)").matches + ) + return "dark" + return "light" +} + +// --------------------------------------------------------------------------- +// Minimal change computation — shared by updateOriginal / updateModified +// --------------------------------------------------------------------------- + +/** + * Compute the smallest `{from, to, insert}` that transforms `oldText` into + * `newText`, via common-prefix / common-suffix scan. Returns `null` when the + * two strings are identical (no change needed). + * + * This gives CM6 a focused change description so its internal scroll mapping + * adjusts the viewport correctly — a full-document replace (`{from: 0, + * to: doc.length, insert: newText}`) would reset scroll to the top. + */ +export function minimalChanges( + oldText: string, + newText: string, +): { from: number; to: number; insert: string } | null { + if (oldText === newText) return null + + // Find common prefix length + let prefix = 0 + const minLen = Math.min(oldText.length, newText.length) + while (prefix < minLen && oldText[prefix] === newText[prefix]) prefix++ + + // Find common suffix length (must not overlap with prefix) + let oldSuffix = oldText.length + let newSuffix = newText.length + while ( + oldSuffix > prefix && + newSuffix > prefix && + oldText[oldSuffix - 1] === newText[newSuffix - 1] + ) { + oldSuffix-- + newSuffix-- + } + + return { + from: prefix, + to: oldSuffix, + insert: newText.slice(prefix, newSuffix), + } +} + +// --------------------------------------------------------------------------- +// Scroll preservation helper +// --------------------------------------------------------------------------- + +/** + * Walk up from `el` to find the nearest ancestor with a non-zero scrollTop. + * Returns the element and its scrollTop, or null if nothing is scrolled. + * + * Used by the SolidJS wrapper to save/restore the parent scroll container's + * position across CM6 teardown/recreate cycles — the container's innerHTML + * clear resets the parent's scrollTop before any async event can capture it. + */ +export function findScrollParent( + el: HTMLElement, +): { element: HTMLElement; scrollTop: number } | null { + let node: HTMLElement | null = el.parentElement + while (node) { + if (node.scrollTop > 0) { + return { element: node, scrollTop: node.scrollTop } + } + node = node.parentElement + } + return null +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view.tsx new file mode 100644 index 00000000..90fc26ea --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/editable-diff-view.tsx @@ -0,0 +1,175 @@ +/** + * EditableDiffView — SolidJS component wrapping CM6 split/unified diff editor. + * + * Auto-exported as @opencode-ai/session-ui/v2/editable-diff-view via the + * wildcard export in session-ui/package.json. + * + * @module + */ + +import { + createEffect, + createSignal, + onCleanup, + onMount, + untrack, + type JSX, +} from "solid-js" +import type { LanguageSupport } from "@codemirror/language" +import { + loadLanguage, + buildThemeExtension, + createDiffEditor, + detectMode, + findScrollParent, + type DiffEditorHandle, +} from "./editable-diff-view-core" + +// Re-export core utilities for external consumers +export { + loadLanguage, + buildThemeExtension, + createDiffEditor, + detectMode, + findScrollParent, + type DiffEditorHandle, +} from "./editable-diff-view-core" + +// --------------------------------------------------------------------------- +// Component props +// --------------------------------------------------------------------------- + +export type EditableDiffViewProps = { + /** The original (left/before) content. */ + original: string + /** The modified (right/after) content — this side is editable. */ + modified: string + /** File extension (without dot) for syntax highlighting, e.g. "ts", "py". */ + language: string + /** "split" for side-by-side, "unified" for interleaved. */ + diffStyle: "unified" | "split" + /** Disable editing (both panes read-only). */ + readOnly: boolean + /** Called on every edit with the full editor content. */ + onChange: (content: string) => void + /** + * Called when the user reverts — the component replaces the document with + * `original` and clears the undo history before invoking this callback. + */ + onRevert: () => void + /** Optional ref callback for the container element. */ + ref?: (el: HTMLElement) => void +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export function EditableDiffView(props: EditableDiffViewProps): JSX.Element { + let containerRef!: HTMLDivElement + let handle: DiffEditorHandle | null = null + const [langSupport, setLangSupport] = createSignal( + null, + ) + + // Load language support asynchronously + onMount(async () => { + const lang = await loadLanguage(props.language) + setLangSupport(lang) + }) + + // ----------------------------------------------------------------------- + // CREATION EFFECT — runs only on structural changes that require a new + // editor: diffStyle (split ↔ unified) and language grammar. + // Content and readOnly changes are handled in-place by effects below. + // ----------------------------------------------------------------------- + createEffect(() => { + // Track only the structural dependencies + const diffStyle = props.diffStyle + const lang = langSupport() + const mode = detectMode() + const theme = buildThemeExtension(mode) + + // Read content and readOnly WITHOUT tracking — we don't want these + // changes to re-run this effect and destroy the editor. + const original = untrack(() => props.original) + const modified = untrack(() => props.modified) + const readOnly = untrack(() => props.readOnly) + const onChange = untrack(() => props.onChange) + + // Tear down previous editor + if (handle) { + handle.destroy() + handle = null + } + + if (!containerRef) return + containerRef.innerHTML = "" + + handle = createDiffEditor({ + parent: containerRef, + original, + modified, + diffStyle, + readOnly, + theme, + language: lang, + onChange: readOnly ? undefined : onChange, + }) + }) + + // ----------------------------------------------------------------------- + // CONTENT EFFECT — runs when props.original or props.modified change. + // Dispatches in-place CM6 transactions via minimalChanges — no editor + // teardown, no DOM disruption, no scroll displacement. + // ----------------------------------------------------------------------- + createEffect(() => { + // Track the content dependencies + const original = props.original + const modified = props.modified + + // Only dispatch if the editor exists (creation effect ran first) + if (!handle) return + + handle.updateOriginal(original) + handle.updateModified(modified) + }) + + // ----------------------------------------------------------------------- + // READONLY EFFECT — runs when props.readOnly changes. + // Reconfigures the Compartment in-place — no editor teardown, no scroll + // displacement. Passes onChange when switching to editable. + // ----------------------------------------------------------------------- + createEffect(() => { + const readOnly = props.readOnly + + if (!handle) return + + handle.setReadOnly(readOnly, readOnly ? undefined : untrack(() => props.onChange)) + }) + + onCleanup(() => { + if (handle) { + handle.destroy() + handle = null + } + }) + + return ( +
{ + containerRef = el + props.ref?.(el) + }} + class="editable-diff-view" + data-diff-style={props.diffStyle} + data-read-only={props.readOnly ? "" : undefined} + style={{ + width: "100%", + height: "100%", + overflow: "auto", + position: "relative", + }} + /> + ) as JSX.Element +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/file-name-picker.test.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/file-name-picker.test.tsx index ccd1099b..45e001f3 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/file-name-picker.test.tsx +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/file-name-picker.test.tsx @@ -82,13 +82,25 @@ function FileNameWithPicker(props: { dropdown.className = "session-review-v2-file-picker-dropdown" dropdown.setAttribute("data-testid", "file-picker-dropdown") - // Render the filePicker content + // Scroll-inner wrapper (mirrors real component) + const scrollInner = document.createElement("div") + scrollInner.className = "session-review-v2-file-picker-scroll-inner" + + // Render the filePicker content inside the scroll wrapper const pickerContent = props.filePicker!({ onSelect }) if (pickerContent instanceof Node) { - dropdown.appendChild(pickerContent) + scrollInner.appendChild(pickerContent) } + dropdown.appendChild(scrollInner) dropdownContainer.appendChild(dropdown) + + // Cap max-width to remaining panel space (mirrors real component's ref callback) + requestAnimationFrame(() => { + const left = dropdown.getBoundingClientRect().left + const available = window.innerWidth - left - 16 + dropdown.style.maxWidth = `${Math.max(200, available)}px` + }) }) return wrapper @@ -168,6 +180,72 @@ describe("FileNameWithPicker (render prop)", () => { cleanup() }) + test("dropdown wraps filePicker content in a scroll-inner container", () => { + const { host, cleanup } = mount({ + file: "src/deeply/nested/path/to/component.tsx", + filePicker: ({ onSelect }) => { + const div = document.createElement("div") + div.setAttribute("data-testid", "mock-tree") + div.textContent = "tree content" + return div + }, + onSelectFile: () => {}, + }) + + // Open dropdown + const trigger = host.querySelector("[data-testid='file-picker-trigger']") as HTMLButtonElement + trigger.click() + + const dropdown = host.querySelector("[data-testid='file-picker-dropdown']") + expect(dropdown).not.toBeNull() + + // The dropdown's direct child should be the scroll-inner wrapper + const scrollInner = dropdown!.querySelector(".session-review-v2-file-picker-scroll-inner") + expect(scrollInner).not.toBeNull() + + // The filePicker content should be inside the scroll-inner, not directly in the dropdown + const tree = scrollInner!.querySelector("[data-testid='mock-tree']") + expect(tree).not.toBeNull() + expect(tree!.textContent).toBe("tree content") + + cleanup() + }) + + test("dropdown max-width is capped to remaining panel width", async () => { + Object.defineProperty(window, "innerWidth", { value: 450, configurable: true }) + + const { host, cleanup } = mount({ + file: "src/foo.ts", + filePicker: ({ onSelect }) => { + const div = document.createElement("div") + div.setAttribute("data-testid", "mock-tree") + return div + }, + onSelectFile: () => {}, + }) + + const trigger = host.querySelector("[data-testid='file-picker-trigger']") as HTMLButtonElement + trigger.click() + + const dropdown = host.querySelector("[data-testid='file-picker-dropdown']") as HTMLElement + expect(dropdown).not.toBeNull() + + // Simulate the dropdown sitting at x=120 from the panel's left edge + dropdown.getBoundingClientRect = () => ({ + left: 120, top: 40, right: 520, bottom: 200, + width: 400, height: 160, x: 120, y: 40, + toJSON() { return this }, + }) + + // Wait for the ref callback's requestAnimationFrame + await new Promise((r) => setTimeout(r, 50)) + + // Should cap at (450 - 120 - 16) = 314px + expect(dropdown.style.maxWidth).toBe("314px") + + cleanup() + }) + test("falls back to static filename when no filePicker provided", () => { const { host, cleanup } = mount({ file: "src/foo.ts", diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx index e2d57254..e96adeaa 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx @@ -3,7 +3,6 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { useI18n } from "@opencode-ai/ui/context/i18n" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" @@ -41,9 +40,6 @@ export type PromptInputV2Props = { borderUnderlay?: boolean class?: string modelControl?: JSX.Element - variantControlVisible?: boolean - attachKeybind?: string[] - attachShortcut?: string /** * Optional control rendered in the bottom row's right anchor, immediately * left of the send button (amicode/opencode#116: the report-a-bug button). @@ -51,10 +47,12 @@ export type PromptInputV2Props = { * nothing renders — the row's layout does not shift. */ trailingControl?: JSX.Element + variantControlVisible?: boolean + attachKeybind?: string[] + attachShortcut?: string } export function PromptInputV2(props: PromptInputV2Props) { - const i18n = useI18n() const state = props.controller.state const view = props.controller.view let editor: HTMLDivElement | undefined @@ -96,14 +94,14 @@ export function PromptInputV2(props: PromptInputV2Props) { />
- {i18n.t("ui.promptInput.dropFiles")} + Drop files to attach
@@ -142,7 +140,7 @@ export function PromptInputV2(props: PromptInputV2Props) { attachments={props.controller.attachments()} comments={props.controller.comments()} activeCommentID={state.activeContextID} - removeLabel={i18n.t("ui.promptInput.removeAttachment")} + removeLabel="Remove attachment" onAttachmentClick={props.controller.openAttachment} onAttachmentRemove={(attachment) => props.controller.removeAttachment(attachment.id)} onCommentClick={(comment) => props.controller.toggleContext(comment.key)} @@ -160,7 +158,7 @@ export function PromptInputV2(props: PromptInputV2Props) { data-component="prompt-input" role="textbox" aria-multiline="true" - aria-label={i18n.t("ui.promptInput.label")} + aria-label="Prompt" contenteditable={!props.disabled && !props.readOnly} autocapitalize={state.mode === "normal" ? "sentences" : "off"} autocorrect={state.mode === "normal" ? "on" : "off"} @@ -195,9 +193,7 @@ export function PromptInputV2(props: PromptInputV2Props) { classList={{ "font-mono!": state.mode === "shell" }} > {view.placeholder?.() ?? - (state.mode === "shell" - ? i18n.t("ui.promptInput.placeholder.shell") - : i18n.t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" }))} + (state.mode === "shell" ? "Enter shell command..." : "Ask anything, / for commands, @ for context...")}
@@ -211,13 +207,13 @@ export function PromptInputV2(props: PromptInputV2Props) { > {(control) => ( - + )} {(control) => ( ( 1}> @@ -268,8 +260,8 @@ export function PromptInputV2(props: PromptInputV2Props) { mode={state.mode} stopping={view.submit.stopping()} disabled={!props.controller.canSubmit()} - sendLabel={i18n.t("ui.promptInput.send")} - stopLabel={i18n.t("ui.promptInput.stop")} + sendLabel="Send" + stopLabel="Interrupt (Esc)" onSubmit={props.controller.submit} onStop={props.controller.stop} /> @@ -395,7 +387,6 @@ export function PromptInputV2Attachments(props: { onCommentClick?: (comment: PromptInputV2Comment) => void onCommentRemove?: (comment: PromptInputV2Comment) => void }) { - const i18n = useI18n() return ( 0 || (props.comments?.length ?? 0) > 0}>
@@ -423,7 +414,7 @@ export function PromptInputV2Attachments(props: {
@@ -590,7 +581,7 @@ export function PromptInputV2Select(props: { {props.options.find((option) => option.id === props.current)?.label ?? props.current} - + @@ -656,7 +647,7 @@ export function PromptInputV2Popover(props: { + + +
+
+ + +
+ + ) + } return ( <> @@ -319,8 +565,69 @@ export function SessionReviewFilePreviewV2(props: SessionReviewFilePreviewV2Prop
+ + + + + + + + {saveStatus() === "saving" + ? "Saving…" + : saveStatus() === "saved" + ? "Saved" + : saveStatus() === "error" + ? "Save failed" + : ""} + +
+ +
+
+ +
+
+
{ scrollRef = el @@ -411,8 +718,19 @@ function FileNameWithPicker(props: { -
- {props.filePicker!({ onSelect })} +
{ + requestAnimationFrame(() => { + const left = el.getBoundingClientRect().left + const available = window.innerWidth - left - 16 + el.style.maxWidth = `${Math.max(200, available)}px` + }) + }} + class="session-review-v2-file-picker-dropdown" + > +
+ {props.filePicker!({ onSelect })} +
diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css index 1f7e469e..829d02de 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css @@ -432,6 +432,27 @@ --line-comment-open-z: 6; } +[data-component="session-review-v2"] [data-slot="session-review-v2-lock-indicator"] { + position: absolute; + top: 8px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + background: var(--accent); + color: var(--accent-ink); + border-radius: var(--radius-md, 8px); + pointer-events: none; + animation: session-review-v2-lock-fade-in 200ms ease both; +} + +@keyframes session-review-v2-lock-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + [data-component="session-review-v2"] .session-review-v2-toolbar-group--segments { gap: 12px; } @@ -573,16 +594,23 @@ z-index: var(--z-overlay, 100); padding: 4px; max-height: 320px; - min-width: 200px; - max-width: 400px; overflow-x: auto; overflow-y: auto; + overscroll-behavior: contain; border-radius: var(--radius-md, 8px); background-color: var(--surface-raised-stronger-non-alpha, var(--v2-background-bg-layer-01)); border: 1px solid color-mix(in oklch, var(--border-base, var(--v2-border-border-base)) 50%, transparent); box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,0.15)); } +/* Scroll-inner wrapper: min-width: max-content ensures the wrapper is at + least as wide as its widest child. When that exceeds the dropdown's + capped width, overflow-x: auto on the dropdown provides horizontal + scrolling. Block items inside fill the wrapper width naturally. */ +.session-review-v2-file-picker-scroll-inner { + min-width: max-content; +} + /* File picker popover content (legacy, kept for compat) */ .session-review-v2-file-picker-content { padding: 4px !important; @@ -605,8 +633,6 @@ line-height: 1.4; color: var(--v2-text-text-muted); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; border-bottom: 1px solid color-mix(in oklch, var(--border-base, var(--v2-border-border-weak)) 30%, transparent); margin-bottom: 2px; } @@ -626,7 +652,6 @@ display: flex; align-items: center; gap: 6px; - width: 100%; padding: 4px 8px; border: none; border-radius: var(--radius-xs, 4px); diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx index c2699119..e4e141ab 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -151,7 +151,8 @@ export function SessionReviewV2(props: SessionReviewV2Props) { const i18n = useI18n() createEffect(() => { - getWorkerPool(props.diffStyle) + const style = props.diffStyle + getWorkerPool(style === "preview" ? undefined : style) }) const fileIndex = () => { @@ -265,8 +266,8 @@ export function SessionReviewV2(props: SessionReviewV2Props) { { - if (value !== "unified" && value !== "split") return - props.onDiffStyleChange?.(value) + if (value !== "unified" && value !== "split" && value !== "preview") return + props.onDiffStyleChange?.(value as any) }} class="session-review-v2-segmented-control session-review-v2-segmented-control--icon" aria-label={i18n.t("ui.sessionReviewV2.diffView")} @@ -281,6 +282,13 @@ export function SessionReviewV2(props: SessionReviewV2Props) { + + + + + + +
diff --git a/packages/app-bundle/overlay/packages/ui/src/context/marked-math.test.ts b/packages/app-bundle/overlay/packages/ui/src/context/marked-math.test.ts index 4e7d7c78..05338bcb 100644 --- a/packages/app-bundle/overlay/packages/ui/src/context/marked-math.test.ts +++ b/packages/app-bundle/overlay/packages/ui/src/context/marked-math.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { Marked } from "marked" import { markedCodeSpanBoundary } from "./marked-code-span" -import { katexExtension, renderMathInText } from "./marked-parser" +import { katexExtension, renderMathInText } from "./marked" const parse = (src: string) => new Marked(markedCodeSpanBoundary, katexExtension).parse(src) diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/ar.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/ar.ts index 9cfafbe9..c00cf7e7 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/ar.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/ar.ts @@ -191,45 +191,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}ث", "ui.message.duration.minutesSeconds": "{{minutes}}د {{seconds}}ث", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "أضف سياقًا لهذا التغيير", - "ui.sessionTurn.diffs.changed.zero": "الملفات المعدلة: {{count}}", - "ui.sessionTurn.diffs.changed.two": "عدد الملفات المعدلة: {{count}}", - "ui.sessionTurn.diffs.changed.few": "الملفات المعدلة: {{count}}", - "ui.sessionTurn.diffs.changed.many": "الملفات المعدلة: {{count}}", - "ui.messagePart.context.read.zero": "{{count}} قراءة", - "ui.messagePart.context.read.two": "عدد القراءات: {{count}}", - "ui.messagePart.context.read.few": "{{count}} قراءات", - "ui.messagePart.context.read.many": "{{count}} قراءةً", - "ui.messagePart.context.search.zero": "{{count}} عملية بحث", - "ui.messagePart.context.search.two": "عدد عمليات البحث: {{count}}", - "ui.messagePart.context.search.few": "{{count}} عمليات بحث", - "ui.messagePart.context.search.many": "{{count}} عملية بحث", - "ui.messagePart.context.list.zero": "{{count}} عملية سرد", - "ui.messagePart.context.list.two": "عدد عمليات السرد: {{count}}", - "ui.messagePart.context.list.few": "{{count}} عمليات سرد", - "ui.messagePart.context.list.many": "{{count}} عملية سرد", - "ui.promptInput.noMatchingItems": "لا توجد عناصر مطابقة", - "ui.promptInput.commands": "الأوامر", - "ui.promptInput.dropFiles": "أفلت الملفات لإرفاقها", - "ui.promptInput.removeAttachment": "إزالة المرفق", - "ui.promptInput.label": "الموجّه", - "ui.promptInput.placeholder.shell": "أدخل أمر shell...", - "ui.promptInput.placeholder.normal": "اسأل عن أي شيء، {{slash}} للأوامر، {{at}} للسياق...", - "ui.promptInput.add": "إضافة صور وملفات", - "ui.promptInput.attachments": "الصور والملفات", - "ui.promptInput.context": "السياق", - "ui.promptInput.shell": "أمر shell", - "ui.promptInput.chooseAgent": "اختيار وكيل", - "ui.promptInput.chooseModel": "اختيار نموذج", - "ui.promptInput.chooseVariant": "اختيار متغير النموذج", - "ui.promptInput.send": "إرسال", - "ui.promptInput.stop": "إيقاف", - "ui.tabs.close": "إغلاق علامة التبويب", - "ui.tool.websearch.provider": "{{provider}} بحث الويب", - "ui.tool.questions.numbered": "أسئلة {{number}}", - "ui.common.clear": "مسح", - "ui.common.file": "ملف", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/br.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/br.ts index d6b039fa..cb3069e0 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/br.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/br.ts @@ -191,33 +191,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Adicionar contexto para esta alteração", - "ui.sessionTurn.diffs.changed.many": "Arquivos alterados: {{count}}", - "ui.messagePart.context.read.many": "{{count}} de leituras", - "ui.messagePart.context.search.many": "{{count}} de pesquisas", - "ui.messagePart.context.list.many": "{{count}} de listas", - "ui.promptInput.noMatchingItems": "Nenhum item correspondente", - "ui.promptInput.commands": "Comandos", - "ui.promptInput.dropFiles": "Solte os arquivos para anexá-los", - "ui.promptInput.removeAttachment": "Remover anexo", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Digite um comando do shell...", - "ui.promptInput.placeholder.normal": "Pergunte qualquer coisa, {{slash}} para comandos, {{at}} para contexto...", - "ui.promptInput.add": "Adicionar imagens e arquivos", - "ui.promptInput.attachments": "Imagens e arquivos", - "ui.promptInput.context": "Contexto", - "ui.promptInput.shell": "Comando do shell", - "ui.promptInput.chooseAgent": "Escolher agente", - "ui.promptInput.chooseModel": "Escolher modelo", - "ui.promptInput.chooseVariant": "Escolher variante do modelo", - "ui.promptInput.send": "Enviar", - "ui.promptInput.stop": "Parar", - "ui.tabs.close": "Fechar aba", - "ui.tool.websearch.provider": "{{provider}} Pesquisa na web", - "ui.tool.questions.numbered": "Perguntas {{number}}", - "ui.common.clear": "Limpar", - "ui.common.file": "Arquivo", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/bs.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/bs.ts index 93d4a7a2..888834b1 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/bs.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/bs.ts @@ -195,33 +195,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Dodaj kontekst za ovu izmjenu", - "ui.sessionTurn.diffs.changed.few": "{{count}} izmijenjene datoteke", - "ui.messagePart.context.read.few": "{{count}} čitanja", - "ui.messagePart.context.search.few": "{{count}} pretrage", - "ui.messagePart.context.list.few": "{{count}} listanja", - "ui.promptInput.noMatchingItems": "Nema odgovarajućih stavki", - "ui.promptInput.commands": "Komande", - "ui.promptInput.dropFiles": "Ispusti datoteke da ih priložiš", - "ui.promptInput.removeAttachment": "Ukloni prilog", - "ui.promptInput.label": "Upit", - "ui.promptInput.placeholder.shell": "Unesi shell komandu...", - "ui.promptInput.placeholder.normal": "Pitaj bilo šta, {{slash}} za komande, {{at}} za kontekst...", - "ui.promptInput.add": "Dodaj slike i datoteke", - "ui.promptInput.attachments": "Slike i datoteke", - "ui.promptInput.context": "Kontekst", - "ui.promptInput.shell": "Shell komanda", - "ui.promptInput.chooseAgent": "Odaberi agenta", - "ui.promptInput.chooseModel": "Odaberi model", - "ui.promptInput.chooseVariant": "Odaberi varijantu modela", - "ui.promptInput.send": "Pošalji", - "ui.promptInput.stop": "Zaustavi", - "ui.tabs.close": "Zatvori karticu", - "ui.tool.websearch.provider": "{{provider}} Pretraga weba", - "ui.tool.questions.numbered": "Pitanja {{number}}", - "ui.common.clear": "Očisti", - "ui.common.file": "Datoteka", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/da.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/da.ts index 57df9891..39540243 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/da.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/da.ts @@ -190,29 +190,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.tool.websearch.provider": "{{provider}} Websøgning", - "ui.tool.questions.numbered": "Spørgsmål {{number}}", - "ui.common.clear": "Ryd", - "ui.common.file": "Fil", - "ui.lineComment.contextPlaceholder": "Tilføj kontekst til denne ændring", - "ui.promptInput.noMatchingItems": "Ingen matchende elementer", - "ui.promptInput.commands": "Kommandoer", - "ui.promptInput.dropFiles": "Slip filer for at vedhæfte dem", - "ui.promptInput.removeAttachment": "Fjern vedhæftet fil", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Indtast shell-kommando...", - "ui.promptInput.placeholder.normal": "Spørg om hvad som helst, {{slash}} for kommandoer, {{at}} for kontekst...", - "ui.promptInput.add": "Tilføj billeder og filer", - "ui.promptInput.attachments": "Billeder og filer", - "ui.promptInput.context": "Kontekst", - "ui.promptInput.shell": "Shell-kommando", - "ui.promptInput.chooseAgent": "Vælg agent", - "ui.promptInput.chooseModel": "Vælg model", - "ui.promptInput.chooseVariant": "Vælg modelvariant", - "ui.promptInput.send": "Send", - "ui.promptInput.stop": "Stop", - "ui.tabs.close": "Luk fane", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/de.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/de.ts index d4acfba2..68a1e050 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/de.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/de.ts @@ -197,29 +197,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.tool.websearch.provider": "{{provider}} Web-Suche", - "ui.tool.questions.numbered": "Fragen {{number}}", - "ui.common.clear": "Leeren", - "ui.common.file": "Datei", - "ui.lineComment.contextPlaceholder": "Kontext zu dieser Änderung hinzufügen", - "ui.promptInput.noMatchingItems": "Keine passenden Einträge", - "ui.promptInput.commands": "Befehle", - "ui.promptInput.dropFiles": "Dateien zum Anhängen hier ablegen", - "ui.promptInput.removeAttachment": "Anhang entfernen", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Shell-Befehl eingeben…", - "ui.promptInput.placeholder.normal": "Beliebige Frage stellen, {{slash}} für Befehle, {{at}} für Kontext…", - "ui.promptInput.add": "Bilder und Dateien hinzufügen", - "ui.promptInput.attachments": "Bilder und Dateien", - "ui.promptInput.context": "Kontext", - "ui.promptInput.shell": "Shell-Befehl", - "ui.promptInput.chooseAgent": "Agenten auswählen", - "ui.promptInput.chooseModel": "Modell auswählen", - "ui.promptInput.chooseVariant": "Modellvariante auswählen", - "ui.promptInput.send": "Senden", - "ui.promptInput.stop": "Stoppen", - "ui.tabs.close": "Tab schließen", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/en.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/en.ts index e36b1f7d..fbbfa2fc 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/en.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/en.ts @@ -194,29 +194,4 @@ export const dict: Record = { "ui.question.multiHint": "Select all answers that apply", "ui.question.singleHint": "Select one answer", "ui.question.custom.placeholder": "Type your answer...", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // absent from the fork-era branded dict. English values verbatim. - "ui.lineComment.contextPlaceholder": "Add context for this change", - "ui.promptInput.noMatchingItems": "No matching items", - "ui.promptInput.commands": "Commands", - "ui.promptInput.dropFiles": "Drop files to attach", - "ui.promptInput.removeAttachment": "Remove attachment", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Enter shell command...", - "ui.promptInput.placeholder.normal": "Ask Amico anything, {{slash}} for commands, {{at}} for context...", - "ui.promptInput.add": "Add images and files", - "ui.promptInput.attachments": "Images and files", - "ui.promptInput.context": "Context", - "ui.promptInput.shell": "Shell command", - "ui.promptInput.chooseAgent": "Choose agent", - "ui.promptInput.chooseModel": "Choose model", - "ui.promptInput.chooseVariant": "Choose model variant", - "ui.promptInput.send": "Send", - "ui.promptInput.stop": "Stop", - "ui.tabs.close": "Close tab", - "ui.tool.websearch.provider": "{{provider}} Web Search", - "ui.tool.questions.numbered": "Questions {{number}}", - "ui.common.clear": "Clear", - "ui.common.file": "File", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/es.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/es.ts index 5f62c1c2..2ba67a7d 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/es.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/es.ts @@ -191,33 +191,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Añadir contexto para este cambio", - "ui.sessionTurn.diffs.changed.many": "{{count}} de archivos modificados", - "ui.messagePart.context.read.many": "{{count}} de lecturas", - "ui.messagePart.context.search.many": "{{count}} de búsquedas", - "ui.messagePart.context.list.many": "{{count}} de listados", - "ui.promptInput.noMatchingItems": "No hay elementos coincidentes", - "ui.promptInput.commands": "Comandos", - "ui.promptInput.dropFiles": "Suelta los archivos para adjuntarlos", - "ui.promptInput.removeAttachment": "Eliminar adjunto", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Introduce un comando de shell...", - "ui.promptInput.placeholder.normal": "Pregunta lo que quieras, {{slash}} para comandos, {{at}} para contexto...", - "ui.promptInput.add": "Añadir imágenes y archivos", - "ui.promptInput.attachments": "Imágenes y archivos", - "ui.promptInput.context": "Contexto", - "ui.promptInput.shell": "Comando de shell", - "ui.promptInput.chooseAgent": "Elegir agente", - "ui.promptInput.chooseModel": "Elegir modelo", - "ui.promptInput.chooseVariant": "Elegir variante del modelo", - "ui.promptInput.send": "Enviar", - "ui.promptInput.stop": "Detener", - "ui.tabs.close": "Cerrar pestaña", - "ui.tool.websearch.provider": "{{provider}} Búsqueda web", - "ui.tool.questions.numbered": "Preguntas {{number}}", - "ui.common.clear": "Borrar", - "ui.common.file": "Archivo", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/fr.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/fr.ts index e1ea9465..44a8ad13 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/fr.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/fr.ts @@ -191,33 +191,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Ajouter du contexte à cette modification", - "ui.sessionTurn.diffs.changed.many": "Fichiers modifiés : {{count}}", - "ui.messagePart.context.read.many": "{{count}} de lectures", - "ui.messagePart.context.search.many": "{{count}} de recherches", - "ui.messagePart.context.list.many": "{{count}} de listes", - "ui.promptInput.noMatchingItems": "Aucun élément correspondant", - "ui.promptInput.commands": "Commandes", - "ui.promptInput.dropFiles": "Déposez des fichiers pour les joindre", - "ui.promptInput.removeAttachment": "Supprimer la pièce jointe", - "ui.promptInput.label": "Invite", - "ui.promptInput.placeholder.shell": "Entrez une commande shell...", - "ui.promptInput.placeholder.normal": "Demandez n'importe quoi, {{slash}} pour les commandes, {{at}} pour le contexte...", - "ui.promptInput.add": "Ajouter des images et des fichiers", - "ui.promptInput.attachments": "Images et fichiers", - "ui.promptInput.context": "Contexte", - "ui.promptInput.shell": "Commande shell", - "ui.promptInput.chooseAgent": "Choisir l'agent", - "ui.promptInput.chooseModel": "Choisir le modèle", - "ui.promptInput.chooseVariant": "Choisir la variante du modèle", - "ui.promptInput.send": "Envoyer", - "ui.promptInput.stop": "Arrêter", - "ui.tabs.close": "Fermer l'onglet", - "ui.tool.websearch.provider": "{{provider}} Recherche Web", - "ui.tool.questions.numbered": "Questions {{number}}", - "ui.common.clear": "Effacer", - "ui.common.file": "Fichier", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/ja.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/ja.ts index 2c14a915..f7a895bb 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/ja.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/ja.ts @@ -190,29 +190,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}秒", "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "この変更に関するコンテキストを追加", - "ui.promptInput.noMatchingItems": "一致する項目がありません", - "ui.promptInput.commands": "コマンド", - "ui.promptInput.dropFiles": "ファイルをドロップして添付", - "ui.promptInput.removeAttachment": "添付ファイルを削除", - "ui.promptInput.label": "プロンプト", - "ui.promptInput.placeholder.shell": "シェルコマンドを入力...", - "ui.promptInput.placeholder.normal": "何でも質問できます。 {{slash}} でコマンド、 {{at}} でコンテキストを追加...", - "ui.promptInput.add": "画像やファイルを追加", - "ui.promptInput.attachments": "画像とファイル", - "ui.promptInput.context": "コンテキスト", - "ui.promptInput.shell": "シェルコマンド", - "ui.promptInput.chooseAgent": "エージェントを選択", - "ui.promptInput.chooseModel": "モデルを選択", - "ui.promptInput.chooseVariant": "モデルバリアントを選択", - "ui.promptInput.send": "送信", - "ui.promptInput.stop": "停止", - "ui.tabs.close": "タブを閉じる", - "ui.tool.websearch.provider": "{{provider}} Web検索", - "ui.tool.questions.numbered": "質問 {{number}}", - "ui.common.clear": "消去", - "ui.common.file": "ファイル", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/ko.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/ko.ts index a07d12be..b10fe560 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/ko.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/ko.ts @@ -192,29 +192,4 @@ export const dict = { "ui.sessionTurn.diffs.showLess": "간략히 표시", "ui.sessionTurn.diffs.more": "+{{count}}개 더 보기", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "이 변경 사항에 대한 컨텍스트 추가", - "ui.promptInput.noMatchingItems": "일치하는 항목 없음", - "ui.promptInput.commands": "명령어", - "ui.promptInput.dropFiles": "첨부할 파일을 놓으세요", - "ui.promptInput.removeAttachment": "첨부 파일 제거", - "ui.promptInput.label": "프롬프트", - "ui.promptInput.placeholder.shell": "셸 명령어 입력...", - "ui.promptInput.placeholder.normal": "무엇이든 물어보세요. {{slash}} 명령어, {{at}} 컨텍스트...", - "ui.promptInput.add": "이미지 및 파일 추가", - "ui.promptInput.attachments": "이미지 및 파일", - "ui.promptInput.context": "컨텍스트", - "ui.promptInput.shell": "셸 명령", - "ui.promptInput.chooseAgent": "에이전트 선택", - "ui.promptInput.chooseModel": "모델 선택", - "ui.promptInput.chooseVariant": "모델 변형 선택", - "ui.promptInput.send": "전송", - "ui.promptInput.stop": "중지", - "ui.tabs.close": "탭 닫기", - "ui.tool.websearch.provider": "{{provider}} 웹 검색", - "ui.tool.questions.numbered": "질문 {{number}}", - "ui.common.clear": "지우기", - "ui.common.file": "파일", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/no.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/no.ts index 7536e8d4..ae0d85ff 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/no.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/no.ts @@ -195,29 +195,4 @@ export const dict: Record = { "ui.sessionTurn.diffs.showLess": "Vis færre", "ui.sessionTurn.diffs.more": "+{{count}} til", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Legg til kontekst for denne endringen", - "ui.promptInput.noMatchingItems": "Ingen treff", - "ui.promptInput.commands": "Kommandoer", - "ui.promptInput.dropFiles": "Slipp filer for å legge dem ved", - "ui.promptInput.removeAttachment": "Fjern vedlegg", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Skriv inn shell-kommando...", - "ui.promptInput.placeholder.normal": "Spør om hva som helst, {{slash}} for kommandoer, {{at}} for kontekst...", - "ui.promptInput.add": "Legg til bilder og filer", - "ui.promptInput.attachments": "Bilder og filer", - "ui.promptInput.context": "Kontekst", - "ui.promptInput.shell": "Shell-kommando", - "ui.promptInput.chooseAgent": "Velg agent", - "ui.promptInput.chooseModel": "Velg modell", - "ui.promptInput.chooseVariant": "Velg modellvariant", - "ui.promptInput.send": "Send", - "ui.promptInput.stop": "Stopp", - "ui.tabs.close": "Lukk fane", - "ui.tool.websearch.provider": "{{provider}} Nettsøk", - "ui.tool.questions.numbered": "Spørsmål {{number}}", - "ui.common.clear": "Tøm", - "ui.common.file": "Fil", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/pl.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/pl.ts index 3742f59e..a4de5714 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/pl.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/pl.ts @@ -190,37 +190,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Dodaj kontekst tej zmiany", - "ui.sessionTurn.diffs.changed.few": "{{count}} zmienione pliki", - "ui.sessionTurn.diffs.changed.many": "{{count}} zmienionych plików", - "ui.messagePart.context.read.few": "Liczba odczytów: {{count}}", - "ui.messagePart.context.read.many": "Liczba odczytów: {{count}}", - "ui.messagePart.context.search.few": "Liczba wyszukiwań: {{count}}", - "ui.messagePart.context.search.many": "Liczba wyszukiwań: {{count}}", - "ui.messagePart.context.list.few": "Liczba list: {{count}}", - "ui.messagePart.context.list.many": "Liczba list: {{count}}", - "ui.promptInput.noMatchingItems": "Brak pasujących elementów", - "ui.promptInput.commands": "Polecenia", - "ui.promptInput.dropFiles": "Upuść pliki, aby je załączyć", - "ui.promptInput.removeAttachment": "Usuń załącznik", - "ui.promptInput.label": "Prompt", - "ui.promptInput.placeholder.shell": "Wpisz polecenie powłoki...", - "ui.promptInput.placeholder.normal": "Zapytaj o cokolwiek, {{slash}} aby wyświetlić polecenia, {{at}} aby wyświetlić kontekst...", - "ui.promptInput.add": "Dodaj obrazy i pliki", - "ui.promptInput.attachments": "Obrazy i pliki", - "ui.promptInput.context": "Kontekst", - "ui.promptInput.shell": "Polecenie powłoki", - "ui.promptInput.chooseAgent": "Wybierz agenta", - "ui.promptInput.chooseModel": "Wybierz model", - "ui.promptInput.chooseVariant": "Wybierz wariant modelu", - "ui.promptInput.send": "Wyślij", - "ui.promptInput.stop": "Zatrzymaj", - "ui.tabs.close": "Zamknij kartę", - "ui.tool.websearch.provider": "{{provider}} Wyszukiwanie w sieci", - "ui.tool.questions.numbered": "Pytania {{number}}", - "ui.common.clear": "Wyczyść", - "ui.common.file": "Plik", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/ru.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/ru.ts index c2d08be7..448c2075 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/ru.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/ru.ts @@ -190,37 +190,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}с", "ui.message.duration.minutesSeconds": "{{minutes}}м {{seconds}}с", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Добавить контекст для этого изменения", - "ui.sessionTurn.diffs.changed.few": "{{count}} изменённых файла", - "ui.sessionTurn.diffs.changed.many": "{{count}} изменённых файлов", - "ui.messagePart.context.read.few": "{{count}} чтения", - "ui.messagePart.context.read.many": "{{count}} чтений", - "ui.messagePart.context.search.few": "{{count}} поиска", - "ui.messagePart.context.search.many": "{{count}} поисков", - "ui.messagePart.context.list.few": "{{count}} списка", - "ui.messagePart.context.list.many": "{{count}} списков", - "ui.promptInput.noMatchingItems": "Нет совпадений", - "ui.promptInput.commands": "Команды", - "ui.promptInput.dropFiles": "Перетащите файлы, чтобы прикрепить их", - "ui.promptInput.removeAttachment": "Удалить вложение", - "ui.promptInput.label": "Промпт", - "ui.promptInput.placeholder.shell": "Введите команду оболочки...", - "ui.promptInput.placeholder.normal": "Спросите что угодно, {{slash}} — команды, {{at}} — контекст...", - "ui.promptInput.add": "Добавить изображения и файлы", - "ui.promptInput.attachments": "Изображения и файлы", - "ui.promptInput.context": "Контекст", - "ui.promptInput.shell": "Команда оболочки", - "ui.promptInput.chooseAgent": "Выбрать агента", - "ui.promptInput.chooseModel": "Выбрать модель", - "ui.promptInput.chooseVariant": "Выбрать вариант модели", - "ui.promptInput.send": "Отправить", - "ui.promptInput.stop": "Остановить", - "ui.tabs.close": "Закрыть вкладку", - "ui.tool.websearch.provider": "{{provider}} Веб-поиск", - "ui.tool.questions.numbered": "Вопросы {{number}}", - "ui.common.clear": "Очистить", - "ui.common.file": "Файл", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/th.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/th.ts index cf1dd849..8bece1b5 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/th.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/th.ts @@ -192,29 +192,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}วิ", "ui.message.duration.minutesSeconds": "{{minutes}}นาที {{seconds}}วิ", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "เพิ่มบริบทสำหรับการเปลี่ยนแปลงนี้", - "ui.promptInput.noMatchingItems": "ไม่พบรายการที่ตรงกัน", - "ui.promptInput.commands": "คำสั่ง", - "ui.promptInput.dropFiles": "วางไฟล์เพื่อแนบ", - "ui.promptInput.removeAttachment": "เอาไฟล์แนบออก", - "ui.promptInput.label": "พรอมต์", - "ui.promptInput.placeholder.shell": "ป้อนคำสั่งเชลล์...", - "ui.promptInput.placeholder.normal": "ถามอะไรก็ได้ {{slash}} สำหรับคำสั่ง {{at}} สำหรับบริบท...", - "ui.promptInput.add": "เพิ่มรูปภาพและไฟล์", - "ui.promptInput.attachments": "รูปภาพและไฟล์", - "ui.promptInput.context": "บริบท", - "ui.promptInput.shell": "คำสั่งเชลล์", - "ui.promptInput.chooseAgent": "เลือกเอเจนต์", - "ui.promptInput.chooseModel": "เลือกโมเดล", - "ui.promptInput.chooseVariant": "เลือกรูปแบบโมเดล", - "ui.promptInput.send": "ส่ง", - "ui.promptInput.stop": "หยุด", - "ui.tabs.close": "ปิดแท็บ", - "ui.tool.websearch.provider": "{{provider}} ค้นหาเว็บ", - "ui.tool.questions.numbered": "คำถาม {{number}}", - "ui.common.clear": "ล้าง", - "ui.common.file": "ไฟล์", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/tr.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/tr.ts index 976a7cfa..574edbb0 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/tr.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/tr.ts @@ -197,29 +197,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}sn", "ui.message.duration.minutesSeconds": "{{minutes}}dk {{seconds}}sn", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Bu değişiklik için bağlam ekle", - "ui.promptInput.noMatchingItems": "Eşleşen öğe yok", - "ui.promptInput.commands": "Komutlar", - "ui.promptInput.dropFiles": "Eklemek için dosyaları bırakın", - "ui.promptInput.removeAttachment": "Eki kaldır", - "ui.promptInput.label": "İstem", - "ui.promptInput.placeholder.shell": "Kabuk komutu girin...", - "ui.promptInput.placeholder.normal": "Bir şey sorun, {{slash}} komutlar için, {{at}} bağlam için...", - "ui.promptInput.add": "Görsel ve dosya ekle", - "ui.promptInput.attachments": "Görseller ve dosyalar", - "ui.promptInput.context": "Bağlam", - "ui.promptInput.shell": "Kabuk komutu", - "ui.promptInput.chooseAgent": "Ajan seç", - "ui.promptInput.chooseModel": "Model seç", - "ui.promptInput.chooseVariant": "Model varyantı seç", - "ui.promptInput.send": "Gönder", - "ui.promptInput.stop": "Durdur", - "ui.tabs.close": "Sekmeyi kapat", - "ui.tool.websearch.provider": "{{provider}} Web araması", - "ui.tool.questions.numbered": "Sorular {{number}}", - "ui.common.clear": "Temizle", - "ui.common.file": "Dosya", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/uk.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/uk.ts index 6dd1a5ec..18451f38 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/uk.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/uk.ts @@ -194,37 +194,4 @@ export const dict: Record = { "ui.question.singleHint": "Виберіть одну відповідь", "ui.question.custom.placeholder": "Введіть свою відповідь...", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "Додати контекст для цієї зміни", - "ui.sessionTurn.diffs.changed.few": "Змінені файли: {{count}}", - "ui.sessionTurn.diffs.changed.many": "Змінених файлів: {{count}}", - "ui.messagePart.context.read.few": "{{count}} читання", - "ui.messagePart.context.read.many": "{{count}} читань", - "ui.messagePart.context.search.few": "{{count}} пошуки", - "ui.messagePart.context.search.many": "{{count}} пошуків", - "ui.messagePart.context.list.few": "{{count}} списки", - "ui.messagePart.context.list.many": "{{count}} списків", - "ui.promptInput.noMatchingItems": "Відповідних елементів немає", - "ui.promptInput.commands": "Команди", - "ui.promptInput.dropFiles": "Перетягніть файли, щоб прикріпити", - "ui.promptInput.removeAttachment": "Видалити вкладення", - "ui.promptInput.label": "Запит", - "ui.promptInput.placeholder.shell": "Введіть команду оболонки...", - "ui.promptInput.placeholder.normal": "Запитайте що завгодно, {{slash}} для команд, {{at}} для контексту...", - "ui.promptInput.add": "Додати зображення та файли", - "ui.promptInput.attachments": "Зображення та файли", - "ui.promptInput.context": "Контекст", - "ui.promptInput.shell": "Команда оболонки", - "ui.promptInput.chooseAgent": "Вибрати агента", - "ui.promptInput.chooseModel": "Вибрати модель", - "ui.promptInput.chooseVariant": "Вибрати варіант моделі", - "ui.promptInput.send": "Надіслати", - "ui.promptInput.stop": "Зупинити", - "ui.tabs.close": "Закрити вкладку", - "ui.tool.websearch.provider": "{{provider}} Веб-пошук", - "ui.tool.questions.numbered": "Запитання {{number}}", - "ui.common.clear": "Очистити", - "ui.common.file": "Файл", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/zh.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/zh.ts index acf2b5fc..7b2b4c9f 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/zh.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/zh.ts @@ -194,29 +194,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}秒", "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "添加此更改的上下文", - "ui.promptInput.noMatchingItems": "没有匹配项", - "ui.promptInput.commands": "命令", - "ui.promptInput.dropFiles": "拖放文件以添加附件", - "ui.promptInput.removeAttachment": "移除附件", - "ui.promptInput.label": "提示词", - "ui.promptInput.placeholder.shell": "输入 shell 命令...", - "ui.promptInput.placeholder.normal": "随便问点什么, {{slash}} 可查看命令, {{at}} 可添加上下文...", - "ui.promptInput.add": "添加图片和文件", - "ui.promptInput.attachments": "图片和文件", - "ui.promptInput.context": "上下文", - "ui.promptInput.shell": "Shell 命令", - "ui.promptInput.chooseAgent": "选择智能体", - "ui.promptInput.chooseModel": "选择模型", - "ui.promptInput.chooseVariant": "选择模型变体", - "ui.promptInput.send": "发送", - "ui.promptInput.stop": "停止", - "ui.tabs.close": "关闭标签页", - "ui.tool.websearch.provider": "{{provider}} 网页搜索", - "ui.tool.questions.numbered": "问题 {{number}}", - "ui.common.clear": "清除", - "ui.common.file": "文件", } diff --git a/packages/app-bundle/overlay/packages/ui/src/i18n/zht.ts b/packages/app-bundle/overlay/packages/ui/src/i18n/zht.ts index 098f8c27..8b674f96 100644 --- a/packages/app-bundle/overlay/packages/ui/src/i18n/zht.ts +++ b/packages/app-bundle/overlay/packages/ui/src/i18n/zht.ts @@ -194,29 +194,4 @@ export const dict = { "ui.message.duration.seconds": "{{count}}秒", "ui.message.duration.minutesSeconds": "{{minutes}}分 {{seconds}}秒", "ui.message.copyTrace": "Copy trace", - - // Keys adopted from upstream v1.18.29 at the overlay re-base (#796) — - // upstream translations, absent from the fork-era dict. - "ui.lineComment.contextPlaceholder": "新增此變更的相關資訊", - "ui.promptInput.noMatchingItems": "沒有符合的項目", - "ui.promptInput.commands": "命令", - "ui.promptInput.dropFiles": "拖放檔案以附加", - "ui.promptInput.removeAttachment": "移除附件", - "ui.promptInput.label": "提示詞", - "ui.promptInput.placeholder.shell": "輸入 shell 命令...", - "ui.promptInput.placeholder.normal": "想問什麼都可以, {{slash}} 可使用命令, {{at}} 可加入上下文...", - "ui.promptInput.add": "新增圖片和檔案", - "ui.promptInput.attachments": "圖片和檔案", - "ui.promptInput.context": "上下文", - "ui.promptInput.shell": "shell 命令", - "ui.promptInput.chooseAgent": "選擇代理程式", - "ui.promptInput.chooseModel": "選擇模型", - "ui.promptInput.chooseVariant": "選擇模型變體", - "ui.promptInput.send": "傳送", - "ui.promptInput.stop": "停止", - "ui.tabs.close": "關閉分頁", - "ui.tool.websearch.provider": "{{provider}} 網頁搜尋", - "ui.tool.questions.numbered": "問題 {{number}}", - "ui.common.clear": "清除", - "ui.common.file": "檔案", } diff --git a/packages/app-bundle/overlay/packages/ui/src/v2/components/icon.tsx b/packages/app-bundle/overlay/packages/ui/src/v2/components/icon.tsx index 45073e10..6b73fd5d 100644 --- a/packages/app-bundle/overlay/packages/ui/src/v2/components/icon.tsx +++ b/packages/app-bundle/overlay/packages/ui/src/v2/components/icon.tsx @@ -115,6 +115,14 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + eye: { + viewBox: "0 0 20 20", + body: ``, + }, + lock: { + viewBox: "0 0 16 16", + body: ``, + }, filetree: { viewBox: "0 0 16 16", body: ``, diff --git a/packages/app-bundle/scripts/overlay-sync.mjs b/packages/app-bundle/scripts/overlay-sync.mjs new file mode 100644 index 00000000..541e0c77 --- /dev/null +++ b/packages/app-bundle/scripts/overlay-sync.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +// Overlay ↔ fork sync check and apply. +// +// The fork (~/harmoniqs/opencode or AMICODE_OPENCODE_SRC) is the source of +// truth for all app source files. The overlay (packages/app-bundle/overlay/) +// is a tracking copy. This script detects and fixes drift between them. +// +// node scripts/overlay-sync.mjs --check exit 0 if in sync, 1 if drifted +// node scripts/overlay-sync.mjs --apply copy fork → overlay + update hashes +// +// Resolves the fork path in order: +// 1. AMICODE_OPENCODE_SRC env var +// 2. ../opencode sibling (relative to repo root) +// 3. ~/harmoniqs/opencode +// Skips with exit 0 if no fork clone is found. + +import { createHash } from "node:crypto" +import { + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readdirSync, + readlinkSync, + writeFileSync, +} from "node:fs" +import { homedir } from "node:os" +import { dirname, join, relative } from "node:path" + +const PKG_ROOT = join(import.meta.dirname, "..") +const REPO_ROOT = join(PKG_ROOT, "..", "..") +const OVERLAY_DIR = join(PKG_ROOT, "overlay") +const MANIFEST_PATH = join(PKG_ROOT, "manifest.json") + +// ── Resolve fork path ─────────────────────────────────────────────────────── + +function resolveForkDir() { + const candidates = [ + process.env.AMICODE_OPENCODE_SRC, + join(REPO_ROOT, "..", "opencode"), + join(homedir(), "harmoniqs", "opencode"), + ].filter(Boolean) + + for (const dir of candidates) { + if (dir && existsSync(join(dir, ".git"))) return dir + } + return null +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function sha256(filepath) { + const st = lstatSync(filepath) + if (st.isSymbolicLink()) { + return createHash("sha256").update(readlinkSync(filepath)).digest("hex") + } + return createHash("sha256").update(readFileSync(filepath)).digest("hex") +} + +function walkDir(dir) { + const results = [] + for (const entry of readdirSync(dir, { recursive: true })) { + const full = join(dir, entry.toString()) + const st = lstatSync(full) + if (st.isFile() || st.isSymbolicLink()) { + results.push(entry.toString()) + } + } + return results +} + +// ── Check mode ────────────────────────────────────────────────────────────── + +function check(forkDir) { + const overlayFiles = walkDir(OVERLAY_DIR) + const drifted = [] + const missingInFork = [] + + for (const rel of overlayFiles) { + const overlayPath = join(OVERLAY_DIR, rel) + const forkPath = join(forkDir, rel) + + if (!existsSync(forkPath)) { + missingInFork.push(rel) + continue + } + + const overlayHash = sha256(overlayPath) + const forkHash = sha256(forkPath) + if (overlayHash !== forkHash) { + drifted.push({ rel, overlayHash, forkHash }) + } + } + + return { drifted, missingInFork } +} + +// ── Apply mode ────────────────────────────────────────────────────────────── + +function apply(forkDir) { + const { drifted, missingInFork } = check(forkDir) + + if (drifted.length === 0 && missingInFork.length === 0) { + console.log("[overlay-sync] already in sync — nothing to do") + return 0 + } + + // Read manifest + const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8")) + let updated = 0 + + for (const { rel } of drifted) { + const forkPath = join(forkDir, rel) + const overlayPath = join(OVERLAY_DIR, rel) + + // Ensure parent directory exists + mkdirSync(dirname(overlayPath), { recursive: true }) + + // Copy fork → overlay + copyFileSync(forkPath, overlayPath) + + // Update manifest hash + const newHash = sha256(overlayPath) + if (manifest.files && manifest.files[rel] !== undefined) { + manifest.files[rel] = newHash + } + + console.log(` updated: ${rel}`) + updated++ + } + + for (const rel of missingInFork) { + console.log(` warning: ${rel} exists in overlay but not in fork (class A amicode-only?)`) + } + + if (updated > 0) { + writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n") + console.log(`[overlay-sync] applied ${updated} file(s), manifest.json updated`) + } + + return 0 +} + +// ── Main ──────────────────────────────────────────────────────────────────── + +const mode = process.argv.includes("--apply") ? "apply" : "check" + +const forkDir = resolveForkDir() +if (!forkDir) { + console.log("[overlay-sync] SKIP: no fork clone found (set AMICODE_OPENCODE_SRC)") + process.exit(0) +} + +console.log(`[overlay-sync] fork: ${forkDir}`) + +if (mode === "apply") { + process.exit(apply(forkDir)) +} else { + const { drifted, missingInFork } = check(forkDir) + + if (missingInFork.length > 0) { + console.log(`[overlay-sync] ${missingInFork.length} file(s) in overlay but not in fork:`) + for (const rel of missingInFork.slice(0, 5)) { + console.log(` missing: ${rel}`) + } + if (missingInFork.length > 5) console.log(` ... and ${missingInFork.length - 5} more`) + } + + if (drifted.length > 0) { + console.log(`[overlay-sync] DRIFT: ${drifted.length} file(s) differ between overlay and fork:`) + for (const { rel } of drifted) { + console.log(` ${rel}`) + } + console.log(`\nRun: pnpm --filter @amicode/app-bundle sync:apply`) + process.exit(1) + } + + console.log(`[overlay-sync] PASS: all ${walkDir(OVERLAY_DIR).length} overlay files match the fork`) + process.exit(0) +} diff --git a/packages/extension/scripts/opencode_dev.mjs b/packages/extension/scripts/opencode_dev.mjs index 3d9422f5..c717e76e 100755 --- a/packages/extension/scripts/opencode_dev.mjs +++ b/packages/extension/scripts/opencode_dev.mjs @@ -148,6 +148,18 @@ export function pinFromRelease({ root = PKG_ROOT, tag, ref, download = ghDownloa function build() { const cloneDir = resolveCloneDir(PKG_ROOT); + // Warn if the overlay is stale relative to the fork (non-blocking). + try { + execFileSync("node", [join(PKG_ROOT, "..", "app-bundle", "scripts", "overlay-sync.mjs"), "--check"], { + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (e) { + console.warn( + `[opencode:build] ⚠ overlay drift detected — the overlay does not match the fork.\n` + + ` Run: pnpm --filter @amicode/app-bundle sync:apply\n` + + ` Building anyway (the binary uses the fork, not the overlay).\n`, + ); + } console.log(`[opencode:build] building from ${cloneDir} with OPENCODE_CHANNEL=dev, re-vendoring…`); // --any-ref: during active dev your clone is off the pinned ref by design. execFileSync("node", [join(PKG_ROOT, "scripts", "fetch_opencode.mjs"), "--local", "--any-ref"], { diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 9062c2a9..9613ee42 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -430,7 +430,7 @@ export class ChatPanel { // (webview-internal origin, never the opencode origin). Forward only // our own envelopes, pinned to the opencode origin. #351 adds // run:*/device:* envelopes for the Work Column inspector tabs. - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, ${origin}); } @@ -587,7 +587,7 @@ export class ChatPanel { } return; } - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, origin); } diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index bb870eb4..945e217b 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -374,7 +374,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // 1. UI surfaces — Workspace sidebar (webview, #673) const sidebarProvider = new SidebarViewProvider(ctx.extensionUri); ctx.subscriptions.push( - vscode.window.registerWebviewViewProvider("amicode.workspace", sidebarProvider), + vscode.window.registerWebviewViewProvider("amicode.workspace", sidebarProvider, { + webviewOptions: { retainContextWhenHidden: true }, + }), ); // Mute the "Chat with Amico" button when a chat panel is open ChatPanel.onLiveChange((count) => sidebarProvider.setChatActive(count > 0)); diff --git a/packages/extension/src/sidebar_bridge.ts b/packages/extension/src/sidebar_bridge.ts index 5547ff29..78f99763 100644 --- a/packages/extension/src/sidebar_bridge.ts +++ b/packages/extension/src/sidebar_bridge.ts @@ -23,7 +23,7 @@ export interface TreeEntry { // ── File operation types ───────────────────────────────────────────────────── export interface FileOpRequest { - op: "rename" | "delete" | "new-file" | "new-folder" | "copy-path" | "copy-relative-path" | "reveal-in-os" | "open-in-terminal" | "open-to-side" | "remove-from-workspace" | "new-session" | "move"; + op: "rename" | "delete" | "new-file" | "new-folder" | "copy-path" | "copy-relative-path" | "reveal-in-os" | "open-in-terminal" | "open-to-side" | "remove-from-workspace" | "new-session" | "move" | "restore"; path: string; newName?: string; name?: string; @@ -33,6 +33,8 @@ export interface FileOpRequest { export interface FileOpResult { ok: boolean; message?: string; + /** Set by move/rename ops — the destination path, for file-op-notify. */ + newPath?: string; } // ── Host → Webview (down) ──────────────────────────────────────────────────── @@ -120,6 +122,8 @@ export interface SidebarMessageHandlers { postMessage: (msg: SidebarDownMessage) => void; setSectionOrder: (order: string[]) => void; reorderRoot: (sourcePath: string, targetPath: string, position: "before" | "after") => void; + /** Notify the chat panel that a file was moved/renamed so Files Changed updates. */ + notifyFileMove?: (oldPath: string, newPath: string, op: string) => void; } /** @@ -181,6 +185,11 @@ export function handleSidebarMessage( op: req.op, path: req.path, }); + // Notify the session page about file moves/renames so Files Changed + // updates the file's path instead of showing a stale ghost entry. + if (result.newPath && (req.op === "move" || req.op === "rename")) { + handlers.notifyFileMove?.(req.path, result.newPath, req.op); + } } }); } diff --git a/packages/extension/src/sidebar_view.ts b/packages/extension/src/sidebar_view.ts index 2c3a7a27..7f063d66 100644 --- a/packages/extension/src/sidebar_view.ts +++ b/packages/extension/src/sidebar_view.ts @@ -13,6 +13,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import { handleSidebarMessage, type SidebarMessageHandlers, type SidebarDownMessage, type FileOpRequest, type FileOpResult, type TreeEntry } from "./sidebar_bridge"; import { SidebarTreeService, type RawDirEntry } from "./sidebar_tree_service"; +import { ChatPanel } from "./chat_panel"; import { detectProjectType } from "./project/detect"; // ── Icon theme resolution ──────────────────────────────────────────────────── @@ -284,6 +285,8 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { private chatActive = false; private activeProjectPath: string | null | undefined = undefined; private watcher?: vscode.FileSystemWatcher; + private fsDebounceTimer?: ReturnType; + private fsPendingFolders = new Set(); private workspaceSub?: vscode.Disposable; private gitSubs: vscode.Disposable[] = []; private treeService: SidebarTreeService; @@ -353,7 +356,7 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { }, getChildren: async (p) => { const entries = await this.treeService.getChildren(p); - return annotateGitStatus(entries); + return annotateGitStatus(entries, p); }, openFile: (p) => { const uri = vscode.Uri.file(p); @@ -365,6 +368,19 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { }, setSectionOrder: (order) => this.setSectionOrder(order), reorderRoot: (sourcePath, targetPath, position) => reorderWorkspaceFolder(sourcePath, targetPath, position), + notifyFileMove: (oldPath, newPath, op) => { + const panel = ChatPanel.peek(); + if (panel) { + void panel.postMessage({ + source: "amicode", + kind: "file-op-notify", + op, + oldPath, + newPath, + home: os.homedir(), + }); + } + }, }; void handleSidebarMessage(msg, handlers); }); @@ -382,6 +398,8 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { }); webviewView.onDidDispose(() => { + clearTimeout(this.fsDebounceTimer); + this.fsPendingFolders.clear(); this.watcher?.dispose(); this.workspaceSub?.dispose(); for (const sub of this.gitSubs) sub.dispose(); @@ -465,10 +483,17 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { const onFsEvent = (uri: vscode.Uri) => { const folder = vscode.workspace.getWorkspaceFolder(uri); if (folder) { - void webviewView.webview.postMessage({ - kind: "fs-changed", - folder: folder.uri.fsPath, - }); + this.fsPendingFolders.add(folder.uri.fsPath); + clearTimeout(this.fsDebounceTimer); + this.fsDebounceTimer = setTimeout(() => { + for (const f of this.fsPendingFolders) { + void webviewView.webview.postMessage({ + kind: "fs-changed", + folder: f, + }); + } + this.fsPendingFolders.clear(); + }, 300); } }; this.watcher.onDidCreate(onFsEvent); @@ -739,6 +764,8 @@ export class SidebarViewProvider implements vscode.WebviewViewProvider { .git-modified { color: var(--vscode-gitDecoration-modifiedResourceForeground, #e2c08d); } .git-added { color: var(--vscode-gitDecoration-addedResourceForeground, #81b88b); } .git-deleted { color: var(--vscode-gitDecoration-deletedResourceForeground, #c74e39); text-decoration: line-through; } + /* Directories inherit the deleted color but not the strikethrough */ + [data-type="directory"] > .tree-node .git-deleted { text-decoration: none; } .git-untracked { color: var(--vscode-gitDecoration-untrackedResourceForeground, #73c991); } .git-ignored { color: var(--vscode-gitDecoration-ignoredResourceForeground, #8c8c8c); opacity: 0.6; } .git-conflict { color: var(--vscode-gitDecoration-conflictingResourceForeground, #e4676b); } @@ -1131,7 +1158,7 @@ export function buildGitStatusMap(api: any): Record { * Annotate tree entries with git status from the Git extension. * Falls back gracefully if the git extension is unavailable. */ -function annotateGitStatus(entries: TreeEntry[]): TreeEntry[] { +function annotateGitStatus(entries: TreeEntry[], parentDir: string): TreeEntry[] { try { const gitExt = vscode.extensions.getExtension("vscode.git"); if (!gitExt?.isActive) return entries; @@ -1143,8 +1170,11 @@ function annotateGitStatus(entries: TreeEntry[]): TreeEntry[] { const statusMap = new Map(Object.entries(statusRecord)); + // Inject ghost entries for files deleted from disk but still tracked by git + const withGhosts = injectDeletedEntries(entries, statusMap, parentDir); + // Annotate files with exact matches, then propagate to directories - const annotated = entries.map((entry) => { + const annotated = withGhosts.map((entry) => { const gitStatus = statusMap.get(entry.path); return gitStatus ? { ...entry, gitStatus: gitStatus as TreeEntry["gitStatus"] } : entry; }); @@ -1154,6 +1184,40 @@ function annotateGitStatus(entries: TreeEntry[]): TreeEntry[] { } } +/** + * Pure function: inject ghost entries for files that git reports as "deleted" + * but are no longer on disk (and therefore missing from the directory listing). + * Only injects direct children of `parentDir` — not nested files. + */ +export function injectDeletedEntries( + entries: TreeEntry[], + statusMap: Map, + parentDir: string, +): TreeEntry[] { + const existingPaths = new Set(entries.map((e) => e.path)); + const prefix = parentDir + "/"; + const ghosts: TreeEntry[] = []; + + for (const [filePath, status] of statusMap) { + if (status !== "deleted") continue; + if (!filePath.startsWith(prefix)) continue; + // Only direct children: no further "/" after the prefix + const remainder = filePath.slice(prefix.length); + if (remainder.includes("/")) continue; + // Don't duplicate an entry that already exists on disk + if (existingPaths.has(filePath)) continue; + + ghosts.push({ + name: remainder, + type: "file", + path: filePath, + gitStatus: "deleted", + }); + } + + return [...entries, ...ghosts]; +} + /** Priority rank for git statuses (higher = more notable). */ const GIT_STATUS_PRIORITY: Record = { conflict: 5, modified: 4, deleted: 3, untracked: 2, added: 1, ignored: 0, @@ -1247,7 +1311,7 @@ export async function executeFileOp(req: FileOpRequest): Promise { // Target doesn't exist — safe to rename } await vscode.workspace.fs.rename(uri, newUri); - return { ok: true }; + return { ok: true, newPath: newUri.fsPath }; } case "move": { if (!req.targetDir) return { ok: false, message: "No target directory" }; @@ -1261,7 +1325,7 @@ export async function executeFileOp(req: FileOpRequest): Promise { // Target doesn't exist — safe to move } await vscode.workspace.fs.rename(uri, targetUri); - return { ok: true }; + return { ok: true, newPath: targetUri.fsPath }; } case "delete": { // Confirmation dialog — same pattern as VS Code's Explorer. @@ -1322,6 +1386,17 @@ export async function executeFileOp(req: FileOpRequest): Promise { void vscode.commands.executeCommand("amicode.newChat"); return { ok: true }; } + case "restore": { + // Restore a git-deleted file by checking it out from HEAD. + const { execFileSync } = require("node:child_process") as typeof import("node:child_process"); + const dirPath = require("node:path").dirname(req.path); + execFileSync("git", ["checkout", "HEAD", "--", req.path], { + cwd: dirPath, + encoding: "utf8", + timeout: 10_000, + }); + return { ok: true }; + } default: return { ok: false, message: `Unknown operation: ${req.op}` }; } diff --git a/packages/extension/src/sidebar_webview.ts b/packages/extension/src/sidebar_webview.ts index 32fa7792..dd96587c 100644 --- a/packages/extension/src/sidebar_webview.ts +++ b/packages/extension/src/sidebar_webview.ts @@ -408,32 +408,43 @@ function createIconEl(icon: string): HTMLElement { // Determine if this is a workspace root const isRoot = currentRoots.some((r) => r.path === nodePath); + // Detect ghost entries (deleted from disk, still tracked by git) + const isGhost = dataEl?.dataset.gitStatus === "deleted"; + // Build menu items interface MenuItem { label?: string; op?: string; separator?: boolean; inline?: boolean } const items: MenuItem[] = []; - if (nodeType === "directory") { - items.push({ label: "New File", op: "new-file", inline: true }); - items.push({ label: "New Folder", op: "new-folder", inline: true }); + if (isGhost) { + // Ghost entries get a reduced menu — only restore and copy paths + items.push({ label: "Restore", op: "restore" }); items.push({ separator: true }); - } - - items.push({ label: "Rename", op: "rename", inline: true }); - items.push({ label: "Delete", op: "delete" }); - items.push({ separator: true }); - items.push({ label: "Copy Path", op: "copy-path" }); - items.push({ label: "Copy Relative Path", op: "copy-relative-path" }); - items.push({ separator: true }); - items.push({ label: "Reveal in Finder", op: "reveal-in-os" }); - items.push({ label: "Open in Terminal", op: "open-in-terminal" }); - - if (nodeType === "file") { - items.push({ label: "Open to the Side", op: "open-to-side" }); - } + items.push({ label: "Copy Path", op: "copy-path" }); + items.push({ label: "Copy Relative Path", op: "copy-relative-path" }); + } else { + if (nodeType === "directory") { + items.push({ label: "New File", op: "new-file", inline: true }); + items.push({ label: "New Folder", op: "new-folder", inline: true }); + items.push({ separator: true }); + } - if (isRoot) { + items.push({ label: "Rename", op: "rename", inline: true }); + items.push({ label: "Delete", op: "delete" }); + items.push({ separator: true }); + items.push({ label: "Copy Path", op: "copy-path" }); + items.push({ label: "Copy Relative Path", op: "copy-relative-path" }); items.push({ separator: true }); - items.push({ label: "Remove from Workspace", op: "remove-from-workspace" }); + items.push({ label: "Reveal in Finder", op: "reveal-in-os" }); + items.push({ label: "Open in Terminal", op: "open-in-terminal" }); + + if (nodeType === "file") { + items.push({ label: "Open to the Side", op: "open-to-side" }); + } + + if (isRoot) { + items.push({ separator: true }); + items.push({ label: "Remove from Workspace", op: "remove-from-workspace" }); + } } // Render menu @@ -482,11 +493,57 @@ function createIconEl(icon: string): HTMLElement { let dragSourcePath: string | null = null; let currentDropTarget: HTMLElement | null = null; + // ── Drag auto-scroll ──────────────────────────────────────────────────── + // When dragging near the top/bottom edge of a scrollable section, auto-scroll + // so off-screen drop targets become reachable. + let autoScrollRAF: number | null = null; + let autoScrollSpeed = 0; + let autoScrollTarget: HTMLElement | null = null; + + function startAutoScroll(container: HTMLElement, speed: number): void { + autoScrollTarget = container; + autoScrollSpeed = speed; + if (autoScrollRAF === null) autoScrollTick(); + } + + function autoScrollTick(): void { + if (autoScrollTarget && autoScrollSpeed !== 0) { + autoScrollTarget.scrollTop += autoScrollSpeed; + autoScrollRAF = requestAnimationFrame(autoScrollTick); + } + } + + function clearAutoScroll(): void { + if (autoScrollRAF !== null) { cancelAnimationFrame(autoScrollRAF); autoScrollRAF = null; } + autoScrollSpeed = 0; + autoScrollTarget = null; + } + + function handleDragAutoScroll(e: DragEvent): void { + const container = (e.target as HTMLElement)?.closest(".section-body") as HTMLElement | null; + if (!container) { clearAutoScroll(); return; } + + const rect = container.getBoundingClientRect(); + const threshold = 30; + const maxSpeed = 12; + + if (e.clientY < rect.top + threshold && container.scrollTop > 0) { + const proximity = Math.max(0, 1 - (e.clientY - rect.top) / threshold); + startAutoScroll(container, -maxSpeed * proximity); + } else if (e.clientY > rect.bottom - threshold && container.scrollTop < container.scrollHeight - container.clientHeight) { + const proximity = Math.max(0, 1 - (rect.bottom - e.clientY) / threshold); + startAutoScroll(container, maxSpeed * proximity); + } else { + clearAutoScroll(); + } + } + function clearDropTarget(): void { if (currentDropTarget) { currentDropTarget.classList.remove("drop-target"); currentDropTarget = null; } + clearAutoScroll(); } // ── Sash resize between sections ──────────────────────────────────────────── @@ -732,9 +789,14 @@ function createIconEl(icon: string): HTMLElement { const directStatus = statusMap[nodePath]; if (directStatus) { label.classList.add(`git-${directStatus}`); + // Keep dataset.gitStatus in sync so click/context-menu guards read current state + el.dataset.gitStatus = directStatus; continue; } + // No direct status — clear stale dataset.gitStatus (e.g. file was restored) + delete el.dataset.gitStatus; + // Directory propagation: find the most notable child status const isDir = el.dataset.type === "directory"; if (isDir) { @@ -1239,6 +1301,7 @@ function createIconEl(icon: string): HTMLElement { label.textContent = entry.name; if (entry.gitStatus) { label.classList.add(`git-${entry.gitStatus}`); + row.dataset.gitStatus = entry.gitStatus; } row.appendChild(iconEl); @@ -1249,6 +1312,9 @@ function createIconEl(icon: string): HTMLElement { setupFileDropTarget(row); row.addEventListener("click", () => { + // Ghost entries (deleted from disk) can't be opened — read from DOM so + // the guard reflects applyGitStatus updates (not a stale closure value) + if (row.dataset.gitStatus === "deleted") return; vscode.postMessage({ kind: "open-file", path: entry.path }); }); @@ -1297,6 +1363,7 @@ function createIconEl(icon: string): HTMLElement { el.classList.remove("dragging"); dragSourcePath = null; clearDropTarget(); + clearAutoScroll(); if (dragImage) { dragImage.remove(); dragImage = null; @@ -1315,6 +1382,7 @@ function createIconEl(icon: string): HTMLElement { if (dragSourcePath.startsWith(targetDir + "/")) return; e.preventDefault(); e.dataTransfer!.dropEffect = "move"; + handleDragAutoScroll(e); if (currentDropTarget !== highlight) { clearDropTarget(); currentDropTarget = highlight; @@ -1353,6 +1421,7 @@ function createIconEl(icon: string): HTMLElement { if (dragSourcePath.startsWith(dirPath + "/")) return; e.preventDefault(); e.dataTransfer!.dropEffect = "move"; + handleDragAutoScroll(e); const dirRow = dirContainer.querySelector(":scope > .tree-node") as HTMLElement | null; if (!dirRow) return; if (currentDropTarget !== dirRow) { @@ -1413,6 +1482,7 @@ function createIconEl(icon: string): HTMLElement { e.preventDefault(); e.stopImmediatePropagation(); // Prevent setupDirectoryDropTarget on the SAME element e.dataTransfer!.dropEffect = "move"; + handleDragAutoScroll(e); // Don't show indicator for self-drop if (sourceRoot.path === root.path) { @@ -1453,18 +1523,20 @@ function createIconEl(icon: string): HTMLElement { }); row.addEventListener("drop", (e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - clearRootInsertIndicator(); - clearDropTarget(); - const sourcePath = e.dataTransfer?.getData("text/plain"); if (!sourcePath) return; // Is the source a root? const sourceRoot = currentRoots.find((r) => r.path === sourcePath); - if (!sourceRoot) return; // Not a root — setupDirectoryDropTarget handles file move + if (!sourceRoot) return; // Not a root — let setupDirectoryDropTarget handle file move if (sourceRoot.projectType !== root.projectType) return; + + // Confirmed root-to-root reorder — now block other handlers + e.preventDefault(); + e.stopImmediatePropagation(); + clearRootInsertIndicator(); + clearDropTarget(); + if (sourcePath === root.path) return; // Self-drop no-op const rect = row.getBoundingClientRect(); @@ -1613,6 +1685,35 @@ function createIconEl(icon: string): HTMLElement { // Reactive git coloring: cache + apply. lastGitStatusMap = msg.statusMap ?? {}; applyGitStatus(lastGitStatusMap); + + // Re-request children for expanded directories that contain orphaned + // deleted entries (deleted files in the status map with no matching + // DOM node). This handles the race where the filesystem watcher fires + // before git has updated its working-tree status — by the time this + // git-status push arrives, annotateGitStatus on the host side will + // inject the ghost entries. + const renderedPaths = new Set( + Array.from(treeRoot?.querySelectorAll("[data-path]") ?? []) + .map((el) => (el as HTMLElement).dataset.path) + .filter(Boolean), + ); + const orphanedDirs = new Set(); + for (const [filePath, status] of Object.entries(lastGitStatusMap)) { + if (status !== "deleted") continue; + if (renderedPaths.has(filePath)) continue; + // Find the parent directory of this orphaned deleted file + const lastSlash = filePath.lastIndexOf("/"); + if (lastSlash < 0) continue; + const parentDir = filePath.slice(0, lastSlash); + // Only re-request if the parent is expanded (visible in the tree) + if (expanded[parentDir]) { + orphanedDirs.add(parentDir); + } + } + for (const dir of orphanedDirs) { + delete childrenCache[dir]; + vscode.postMessage({ kind: "get-children", path: dir }); + } break; } @@ -1640,8 +1741,11 @@ function createIconEl(icon: string): HTMLElement { } case "section-order": { - // Host replays the persisted section order on webview resolve + // Host replays the persisted section order on webview resolve. + // Skip the re-render when the order is unchanged — avoids a + // redundant full DOM wipe on sidebar show and section-reorder echo. if (Array.isArray(msg.order)) { + if (JSON.stringify(msg.order) === JSON.stringify(currentSectionOrder)) break; currentSectionOrder = msg.order; saveExpandedState(); // persist to webview state for tab-switch survival // Re-render with the new order if we already have roots diff --git a/packages/extension/test/agents_md.test.ts b/packages/extension/test/agents_md.test.ts index 8cafb514..4cc377d3 100644 --- a/packages/extension/test/agents_md.test.ts +++ b/packages/extension/test/agents_md.test.ts @@ -278,3 +278,25 @@ describe("HP solver-mode guidance: both imports", () => { } }); }); + +// The overlay is a tracking copy of the fork — edits there silently vanish on +// sync:apply and never reach the compiled binary. An agent that edits the overlay +// instead of the fork wastes a whole implementation cycle. The rule must be in +// the ROOT AGENTS.md (the dev-agent instruction set) so every agent sees it +// before touching app-layer files. +describe("AGENTS.md overlay prohibition", () => { + const ROOT_AGENTS = readFileSync(join(__dirname, "..", "..", "..", "AGENTS.md"), "utf8"); + + it("explicitly prohibits editing the overlay directly", () => { + expect(ROOT_AGENTS).toMatch(/[Nn]ever edit the overlay directly/); + }); + + it("names the fork as the correct edit target for app-layer changes", () => { + expect(ROOT_AGENTS).toMatch(/edit.*fork/i); + expect(ROOT_AGENTS).toMatch(/~\/harmoniqs\/opencode|AMICODE_OPENCODE_SRC/); + }); + + it("warns that overlay edits silently vanish", () => { + expect(ROOT_AGENTS).toMatch(/silently (vanish|overwritten|lost)/i); + }); +}); diff --git a/packages/extension/test/edit_to_context.test.ts b/packages/extension/test/edit_to_context.test.ts new file mode 100644 index 00000000..9524eb0f --- /dev/null +++ b/packages/extension/test/edit_to_context.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, test, vi } from "vitest" + +/** + * Tests for #771: Edit-to-context feedback system. + * + * Tests the edit context manager that tracks per-file edit snapshots + * and produces diff context items for the agent. + */ + +// --------------------------------------------------------------------------- +// Simple line diff for tests (the real impl uses the `diff` npm package) +// --------------------------------------------------------------------------- + +function simpleDiff(path: string, original: string, current: string): string { + const origLines = original.split("\n") + const currLines = current.split("\n") + const lines: string[] = [`--- ${path}\t(original)`, `+++ ${path}\t(edited)`, "@@ diff @@"] + const maxLen = Math.max(origLines.length, currLines.length) + for (let i = 0; i < maxLen; i++) { + const o = i < origLines.length ? origLines[i] : undefined + const c = i < currLines.length ? currLines[i] : undefined + if (o === c) { + lines.push(` ${o}`) + } else { + if (o !== undefined) lines.push(`-${o}`) + if (c !== undefined) lines.push(`+${c}`) + } + } + return lines.join("\n") +} + +// --------------------------------------------------------------------------- +// Edit context manager +// --------------------------------------------------------------------------- + +interface EditContextItem { + type: "file" + path: string + comment: string + commentID: string + commentOrigin: "review" +} + +function createEditContextManager() { + const snapshots = new Map() // path → original content + const items = new Map() // path → context item + + return { + get size() { + return items.size + }, + + /** + * Record a file's original content (snapshot taken on first edit). + */ + snapshot(path: string, originalContent: string) { + if (!snapshots.has(path)) { + snapshots.set(path, originalContent) + } + }, + + /** + * Update the edit context for a file with its current content. + * Produces a unified diff between original and current. + */ + update(path: string, currentContent: string) { + const original = snapshots.get(path) + if (original === undefined) return + + // If content matches original, remove the context item + if (original === currentContent) { + items.delete(path) + return + } + + const diff = simpleDiff(path, original, currentContent) + const comment = `The user made the following edits to ${path}:\n\n${diff}` + + items.set(path, { + type: "file", + path, + comment, + commentID: `edit:${path}`, + commentOrigin: "review", + }) + }, + + /** + * Get context items for message send. + */ + getItems(): EditContextItem[] { + return Array.from(items.values()) + }, + + /** + * Clear a single file's edit context (e.g., on revert). + */ + clearFile(path: string) { + items.delete(path) + snapshots.delete(path) + }, + + /** + * Consume all items (remove after send). + */ + consumeAll(): EditContextItem[] { + const result = Array.from(items.values()) + items.clear() + snapshots.clear() + return result + }, + + /** + * Check if a file has pending edits. + */ + hasEdits(path: string): boolean { + return items.has(path) + }, + } +} + +describe("Edit context manager", () => { + test("starts empty", () => { + const mgr = createEditContextManager() + expect(mgr.size).toBe(0) + expect(mgr.getItems()).toEqual([]) + }) + + test("first edit creates a snapshot and context item", () => { + const mgr = createEditContextManager() + mgr.snapshot("src/foo.ts", "original") + mgr.update("src/foo.ts", "modified") + + expect(mgr.size).toBe(1) + const items = mgr.getItems() + expect(items.length).toBe(1) + expect(items[0].path).toBe("src/foo.ts") + expect(items[0].commentOrigin).toBe("review") + expect(items[0].comment).toContain("The user made the following edits to src/foo.ts") + expect(items[0].comment).toContain("original") + expect(items[0].comment).toContain("modified") + }) + + test("continued edits update the same item (one per file)", () => { + const mgr = createEditContextManager() + mgr.snapshot("src/foo.ts", "original") + mgr.update("src/foo.ts", "edit-v1") + mgr.update("src/foo.ts", "edit-v2") + + expect(mgr.size).toBe(1) + const items = mgr.getItems() + expect(items[0].comment).toContain("edit-v2") + expect(items[0].comment).not.toContain("edit-v1") + }) + + test("editing back to original removes the item", () => { + const mgr = createEditContextManager() + mgr.snapshot("src/foo.ts", "original") + mgr.update("src/foo.ts", "modified") + expect(mgr.size).toBe(1) + + mgr.update("src/foo.ts", "original") + expect(mgr.size).toBe(0) + }) + + test("multiple files each get their own item", () => { + const mgr = createEditContextManager() + mgr.snapshot("a.ts", "orig-a") + mgr.snapshot("b.ts", "orig-b") + mgr.update("a.ts", "mod-a") + mgr.update("b.ts", "mod-b") + + expect(mgr.size).toBe(2) + const paths = mgr.getItems().map((i) => i.path).sort() + expect(paths).toEqual(["a.ts", "b.ts"]) + }) + + test("clearFile removes snapshot and item for one file", () => { + const mgr = createEditContextManager() + mgr.snapshot("a.ts", "orig") + mgr.update("a.ts", "mod") + mgr.snapshot("b.ts", "orig") + mgr.update("b.ts", "mod") + + mgr.clearFile("a.ts") + expect(mgr.size).toBe(1) + expect(mgr.hasEdits("a.ts")).toBe(false) + expect(mgr.hasEdits("b.ts")).toBe(true) + }) + + test("consumeAll returns items and clears everything", () => { + const mgr = createEditContextManager() + mgr.snapshot("a.ts", "orig") + mgr.update("a.ts", "mod") + + const consumed = mgr.consumeAll() + expect(consumed.length).toBe(1) + expect(mgr.size).toBe(0) + expect(mgr.getItems()).toEqual([]) + }) + + test("snapshot only records on first call (doesn't overwrite)", () => { + const mgr = createEditContextManager() + mgr.snapshot("a.ts", "first-original") + mgr.snapshot("a.ts", "second-original") + mgr.update("a.ts", "modified") + + const items = mgr.getItems() + // The diff should be against "first-original", not "second-original" + expect(items[0].comment).toContain("first-original") + }) + + test("context items have unique commentIDs per file", () => { + const mgr = createEditContextManager() + mgr.snapshot("a.ts", "o") + mgr.snapshot("b.ts", "o") + mgr.update("a.ts", "m") + mgr.update("b.ts", "m") + + const ids = mgr.getItems().map((i) => i.commentID) + expect(new Set(ids).size).toBe(2) + expect(ids).toContain("edit:a.ts") + expect(ids).toContain("edit:b.ts") + }) + + test("hasEdits returns false for unknown files", () => { + const mgr = createEditContextManager() + expect(mgr.hasEdits("unknown.ts")).toBe(false) + }) + + test("context item comment contains unified diff format", () => { + const mgr = createEditContextManager() + mgr.snapshot("file.ts", "line1\nline2\nline3\n") + mgr.update("file.ts", "line1\nline2-changed\nline3\nline4\n") + + const items = mgr.getItems() + // Should contain diff markers + expect(items[0].comment).toContain("---") + expect(items[0].comment).toContain("+++") + expect(items[0].comment).toContain("@@") + }) +}) diff --git a/packages/extension/test/editable_diffs_wiring.test.ts b/packages/extension/test/editable_diffs_wiring.test.ts new file mode 100644 index 00000000..4b13a107 --- /dev/null +++ b/packages/extension/test/editable_diffs_wiring.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, test, vi, beforeEach } from "vitest" + +/** + * Tests for #768: Wire editable diffs into Files Changed + auto-save. + * + * These tests verify the overlay's integration of EditableDiffView: + * - Save utility logic (debounce, immediate, status transitions) + * - ReviewDiffStyle type widening + * - File status → readOnly mapping + */ + +// --------------------------------------------------------------------------- +// Save utility logic +// --------------------------------------------------------------------------- + +type SaveStatus = "idle" | "saving" | "saved" | "error" + +/** + * Minimal reproduction of the save utility for testable isolation. + * The actual implementation lives in the overlay component. + */ +function createSaveController(opts: { + onSave: (path: string, content: string) => Promise + debounceMs?: number + savedDisplayMs?: number +}) { + const debounceMs = opts.debounceMs ?? 1000 + const savedDisplayMs = opts.savedDisplayMs ?? 2000 + let status: SaveStatus = "idle" + let saveTimer: ReturnType | undefined + let savedTimer: ReturnType | undefined + const listeners: Array<(s: SaveStatus) => void> = [] + + function setStatus(s: SaveStatus) { + status = s + for (const l of listeners) l(s) + } + + return { + get status() { + return status + }, + onStatusChange(cb: (s: SaveStatus) => void) { + listeners.push(cb) + }, + debouncedSave(path: string, content: string) { + if (saveTimer) clearTimeout(saveTimer) + saveTimer = setTimeout(() => { + setStatus("saving") + opts + .onSave(path, content) + .then(() => { + setStatus("saved") + if (savedTimer) clearTimeout(savedTimer) + savedTimer = setTimeout(() => setStatus("idle"), savedDisplayMs) + }) + .catch(() => { + setStatus("error") + if (savedTimer) clearTimeout(savedTimer) + savedTimer = setTimeout(() => setStatus("idle"), savedDisplayMs) + }) + }, debounceMs) + }, + immediateSave(path: string, content: string) { + if (saveTimer) clearTimeout(saveTimer) + setStatus("saving") + opts + .onSave(path, content) + .then(() => { + setStatus("saved") + if (savedTimer) clearTimeout(savedTimer) + savedTimer = setTimeout(() => setStatus("idle"), savedDisplayMs) + }) + .catch(() => { + setStatus("error") + if (savedTimer) clearTimeout(savedTimer) + savedTimer = setTimeout(() => setStatus("idle"), savedDisplayMs) + }) + }, + cleanup() { + if (saveTimer) clearTimeout(saveTimer) + if (savedTimer) clearTimeout(savedTimer) + }, + } +} + +describe("Save controller", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + test("starts at idle", () => { + const ctrl = createSaveController({ onSave: async () => {} }) + expect(ctrl.status).toBe("idle") + ctrl.cleanup() + }) + + test("debouncedSave transitions to saving after debounce period", async () => { + const onSave = vi.fn(async () => {}) + const ctrl = createSaveController({ onSave, debounceMs: 100 }) + const statuses: SaveStatus[] = [] + ctrl.onStatusChange((s) => statuses.push(s)) + + ctrl.debouncedSave("test.ts", "content") + + // Not called yet (within debounce) + expect(onSave).not.toHaveBeenCalled() + + vi.advanceTimersByTime(100) + expect(onSave).toHaveBeenCalledWith("test.ts", "content") + expect(statuses).toContain("saving") + + ctrl.cleanup() + }) + + test("immediateSave cancels pending debounced save and fires immediately", async () => { + const calls: string[] = [] + const onSave = vi.fn(async (_path: string, content: string) => { + calls.push(content) + }) + const ctrl = createSaveController({ onSave, debounceMs: 1000 }) + + ctrl.debouncedSave("test.ts", "debounced-content") + ctrl.immediateSave("test.ts", "immediate-content") + + expect(onSave).toHaveBeenCalledTimes(1) + expect(onSave).toHaveBeenCalledWith("test.ts", "immediate-content") + + // Advance past debounce — should NOT fire the debounced save + vi.advanceTimersByTime(1500) + expect(onSave).toHaveBeenCalledTimes(1) + + ctrl.cleanup() + }) + + test("transitions to saved after successful save, then back to idle", async () => { + let resolvePromise!: () => void + const onSave = vi.fn( + () => new Promise((r) => (resolvePromise = r)), + ) + const ctrl = createSaveController({ + onSave, + debounceMs: 0, + savedDisplayMs: 100, + }) + const statuses: SaveStatus[] = [] + ctrl.onStatusChange((s) => statuses.push(s)) + + ctrl.immediateSave("test.ts", "content") + expect(ctrl.status).toBe("saving") + + // Resolve the save + resolvePromise() + await vi.advanceTimersByTimeAsync(0) + expect(ctrl.status).toBe("saved") + + // After savedDisplayMs, back to idle + vi.advanceTimersByTime(100) + expect(ctrl.status).toBe("idle") + + ctrl.cleanup() + }) + + test("transitions to error on save failure, then back to idle", async () => { + const onSave = vi.fn(async () => { + throw new Error("network error") + }) + const ctrl = createSaveController({ + onSave, + debounceMs: 0, + savedDisplayMs: 100, + }) + + ctrl.immediateSave("test.ts", "content") + await vi.advanceTimersByTimeAsync(0) + expect(ctrl.status).toBe("error") + + vi.advanceTimersByTime(100) + expect(ctrl.status).toBe("idle") + + ctrl.cleanup() + }) + + test("multiple rapid debouncedSave calls only fires once", () => { + const onSave = vi.fn(async () => {}) + const ctrl = createSaveController({ onSave, debounceMs: 100 }) + + ctrl.debouncedSave("test.ts", "v1") + vi.advanceTimersByTime(50) + ctrl.debouncedSave("test.ts", "v2") + vi.advanceTimersByTime(50) + ctrl.debouncedSave("test.ts", "v3") + vi.advanceTimersByTime(100) + + expect(onSave).toHaveBeenCalledTimes(1) + expect(onSave).toHaveBeenCalledWith("test.ts", "v3") + + ctrl.cleanup() + }) +}) + +// --------------------------------------------------------------------------- +// File status → readOnly mapping +// --------------------------------------------------------------------------- + +describe("File status → readOnly mapping", () => { + function isReadOnly(status: "added" | "modified" | "deleted"): boolean { + return status === "deleted" + } + + test("added files are editable", () => { + expect(isReadOnly("added")).toBe(false) + }) + + test("modified files are editable", () => { + expect(isReadOnly("modified")).toBe(false) + }) + + test("deleted files are read-only", () => { + expect(isReadOnly("deleted")).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// ReviewDiffStyle type widening +// --------------------------------------------------------------------------- + +describe("ReviewDiffStyleExtended", () => { + type ReviewDiffStyleExtended = "unified" | "split" | "preview" + + test("accepts unified", () => { + const s: ReviewDiffStyleExtended = "unified" + expect(s).toBe("unified") + }) + + test("accepts split", () => { + const s: ReviewDiffStyleExtended = "split" + expect(s).toBe("split") + }) + + test("accepts preview", () => { + const s: ReviewDiffStyleExtended = "preview" + expect(s).toBe("preview") + }) + + test("base type is assignable to extended", () => { + const base: "unified" | "split" = "split" + const extended: ReviewDiffStyleExtended = base + expect(extended).toBe("split") + }) +}) diff --git a/packages/extension/test/markdown_preview.test.ts b/packages/extension/test/markdown_preview.test.ts new file mode 100644 index 00000000..be3c7ac5 --- /dev/null +++ b/packages/extension/test/markdown_preview.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from "vitest" + +/** + * Tests for #772: Markdown preview toggle. + * + * Tests the preprocessMarkdown utility extraction and the + * diff style fallback logic for preview mode. + */ + +// --------------------------------------------------------------------------- +// preprocessMarkdown — extracted from session-preview-tab.tsx +// --------------------------------------------------------------------------- + +/** + * Convert fenced ```math blocks to $$...$$ display math for KaTeX. + */ +function preprocessMarkdown(md: string): string { + return md.replace(/```math\n([\s\S]*?)```/g, (_, p1) => `$$${p1}$$`) +} + +describe("preprocessMarkdown", () => { + test("converts fenced math blocks to display math", () => { + const input = "text\n```math\nx^2 + y^2 = z^2\n```\nmore text" + const result = preprocessMarkdown(input) + expect(result).toBe("text\n$$x^2 + y^2 = z^2\n$$\nmore text") + }) + + test("handles multiple math blocks", () => { + const input = "```math\na\n```\nmiddle\n```math\nb\n```" + const result = preprocessMarkdown(input) + expect(result).toContain("$$a\n$$") + expect(result).toContain("$$b\n$$") + expect(result).not.toContain("```math") + }) + + test("leaves non-math fenced code blocks untouched", () => { + const input = "```typescript\nconst x = 1\n```" + const result = preprocessMarkdown(input) + expect(result).toBe(input) + }) + + test("handles empty input", () => { + expect(preprocessMarkdown("")).toBe("") + }) + + test("handles input with no math blocks", () => { + const input = "# Hello\nWorld\n- list item" + expect(preprocessMarkdown(input)).toBe(input) + }) +}) + +// --------------------------------------------------------------------------- +// Preview mode visibility logic +// --------------------------------------------------------------------------- + +describe("Preview segment visibility", () => { + function isMarkdownFile(path: string): boolean { + return /\.md$/i.test(path) + } + + test("shows for .md files", () => { + expect(isMarkdownFile("README.md")).toBe(true) + expect(isMarkdownFile("docs/guide.md")).toBe(true) + }) + + test("shows for .MD files (case insensitive)", () => { + expect(isMarkdownFile("FILE.MD")).toBe(true) + }) + + test("hides for non-markdown files", () => { + expect(isMarkdownFile("app.ts")).toBe(false) + expect(isMarkdownFile("style.css")).toBe(false) + expect(isMarkdownFile("data.json")).toBe(false) + expect(isMarkdownFile("script.py")).toBe(false) + }) + + test("hides for files with md in the name but not as extension", () => { + expect(isMarkdownFile("markdown-parser.ts")).toBe(false) + }) +}) + +// --------------------------------------------------------------------------- +// Diff style fallback logic +// --------------------------------------------------------------------------- + +describe("Preview mode fallback", () => { + type DiffStyle = "unified" | "split" | "preview" + + /** + * Resolve the effective diff style: if preview is selected but the file + * isn't markdown, fall back to the last non-preview style. + */ + function resolveEffectiveDiffStyle( + selected: DiffStyle, + isMarkdown: boolean, + lastDiffMode: "unified" | "split", + ): DiffStyle { + if (selected === "preview" && !isMarkdown) { + return lastDiffMode + } + return selected + } + + test("returns preview for .md files when preview is selected", () => { + expect(resolveEffectiveDiffStyle("preview", true, "split")).toBe("preview") + }) + + test("falls back to last diff mode for non-md files when preview selected", () => { + expect(resolveEffectiveDiffStyle("preview", false, "split")).toBe("split") + expect(resolveEffectiveDiffStyle("preview", false, "unified")).toBe("unified") + }) + + test("returns unified/split regardless of file type", () => { + expect(resolveEffectiveDiffStyle("unified", true, "split")).toBe("unified") + expect(resolveEffectiveDiffStyle("split", false, "unified")).toBe("split") + }) +}) diff --git a/packages/extension/test/overlay_sync.test.ts b/packages/extension/test/overlay_sync.test.ts new file mode 100644 index 00000000..2ff655e6 --- /dev/null +++ b/packages/extension/test/overlay_sync.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "vitest" +import { execFileSync } from "node:child_process" +import { join } from "node:path" + +/** + * Tests for overlay-sync.mjs — the overlay ↔ fork sync checker. + * + * These test the script's behavior, not the current sync state (which + * depends on whether other PRs have landed on the fork since the last + * extraction). + */ + +const SCRIPT = join(__dirname, "..", "..", "app-bundle", "scripts", "overlay-sync.mjs") + +describe("overlay-sync script", () => { + test("--check runs without crashing and produces structured output", () => { + // The script exits 0 (PASS/SKIP) or 1 (DRIFT). Both are valid. + // Only a crash (exit > 1 or thrown error) is a test failure. + let stdout: string + let exitCode: number + + try { + stdout = execFileSync("node", [SCRIPT, "--check"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + exitCode = 0 + } catch (e: any) { + stdout = e.stdout ?? "" + exitCode = e.status ?? 1 + } + + // Should produce recognizable output + expect(stdout).toContain("[overlay-sync]") + // Exit code 0 = PASS/SKIP, 1 = DRIFT (both acceptable) + expect(exitCode).toBeLessThanOrEqual(1) + }) + + test("--check reports fork path", () => { + let stdout: string + try { + stdout = execFileSync("node", [SCRIPT, "--check"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + } catch (e: any) { + stdout = e.stdout ?? "" + } + + // Should resolve and report the fork path (or SKIP if not found) + const hasForkPath = stdout.includes("fork:") || stdout.includes("SKIP") + expect(hasForkPath).toBe(true) + }) + + test("--apply runs without crashing", () => { + // We don't actually want to mutate files in the test, so we verify + // the script at least parses and finds the fork. If there's drift, + // it will apply it — that's fine, we want the overlay in sync anyway. + let exitCode: number + let stdout: string + try { + stdout = execFileSync("node", [SCRIPT, "--apply"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + exitCode = 0 + } catch (e: any) { + stdout = e.stdout ?? "" + exitCode = e.status ?? 1 + } + + expect(stdout).toContain("[overlay-sync]") + expect(exitCode).toBe(0) + }) + + test("--check after --apply reports PASS", () => { + // After apply, check should pass (all files now match the fork) + let stdout: string + let exitCode: number + + // First apply + try { + execFileSync("node", [SCRIPT, "--apply"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + } catch { + // apply may exit non-zero if fork not found + } + + // Then check + try { + stdout = execFileSync("node", [SCRIPT, "--check"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + exitCode = 0 + } catch (e: any) { + stdout = e.stdout ?? "" + exitCode = e.status ?? 1 + } + + // After apply, everything should be in sync + const passed = stdout.includes("PASS") || stdout.includes("SKIP") + expect(passed).toBe(true) + expect(exitCode).toBe(0) + }) +}) diff --git a/packages/extension/test/revert_concurrent.test.ts b/packages/extension/test/revert_concurrent.test.ts new file mode 100644 index 00000000..6678990d --- /dev/null +++ b/packages/extension/test/revert_concurrent.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, test, vi, beforeEach } from "vitest" + +/** + * Tests for #770: Revert + concurrent edit handling. + * + * Tests the revert controller and concurrent-edit detection logic + * in isolation from the SolidJS component. + */ + +// --------------------------------------------------------------------------- +// Revert controller +// --------------------------------------------------------------------------- + +function createRevertController(opts: { + getOriginal: () => string + getCurrentContent: () => string + onSave: (path: string, content: string) => Promise + onRevertEditor: () => void +}) { + let hasEdits = false + + return { + get hasEdits() { + return hasEdits + }, + markEdited() { + hasEdits = true + }, + async revert(path: string) { + const original = opts.getOriginal() + // Write original content to disk + await opts.onSave(path, original) + // Reset the editor (clears undo history via the CM6 onRevert callback) + opts.onRevertEditor() + hasEdits = false + }, + reset() { + hasEdits = false + }, + } +} + +describe("Revert controller", () => { + test("hasEdits starts false", () => { + const ctrl = createRevertController({ + getOriginal: () => "original", + getCurrentContent: () => "modified", + onSave: async () => {}, + onRevertEditor: () => {}, + }) + expect(ctrl.hasEdits).toBe(false) + }) + + test("markEdited sets hasEdits to true", () => { + const ctrl = createRevertController({ + getOriginal: () => "original", + getCurrentContent: () => "modified", + onSave: async () => {}, + onRevertEditor: () => {}, + }) + ctrl.markEdited() + expect(ctrl.hasEdits).toBe(true) + }) + + test("revert writes original content to disk", async () => { + const saves: Array<{ path: string; content: string }> = [] + const ctrl = createRevertController({ + getOriginal: () => "original content", + getCurrentContent: () => "user edits", + onSave: async (path, content) => { + saves.push({ path, content }) + }, + onRevertEditor: () => {}, + }) + ctrl.markEdited() + await ctrl.revert("test.ts") + + expect(saves).toEqual([{ path: "test.ts", content: "original content" }]) + }) + + test("revert calls onRevertEditor", async () => { + const editorReverted = vi.fn() + const ctrl = createRevertController({ + getOriginal: () => "orig", + getCurrentContent: () => "mod", + onSave: async () => {}, + onRevertEditor: editorReverted, + }) + ctrl.markEdited() + await ctrl.revert("test.ts") + + expect(editorReverted).toHaveBeenCalledTimes(1) + }) + + test("revert resets hasEdits to false", async () => { + const ctrl = createRevertController({ + getOriginal: () => "orig", + getCurrentContent: () => "mod", + onSave: async () => {}, + onRevertEditor: () => {}, + }) + ctrl.markEdited() + expect(ctrl.hasEdits).toBe(true) + + await ctrl.revert("test.ts") + expect(ctrl.hasEdits).toBe(false) + }) +}) + +// --------------------------------------------------------------------------- +// Concurrent edit detection +// --------------------------------------------------------------------------- + +type ConcurrentEditState = "none" | "detected" + +function createConcurrentEditDetector() { + let state: ConcurrentEditState = "none" + let lastDiffVersion: string | null = null + let userHasEdits = false + + return { + get state() { + return state + }, + setUserHasEdits(v: boolean) { + userHasEdits = v + }, + /** + * Called when a new fileDiff prop arrives. + * Returns true if an external change was detected while user has edits. + */ + onDiffUpdate(diffVersion: string): boolean { + if (lastDiffVersion === null) { + lastDiffVersion = diffVersion + return false + } + if (diffVersion !== lastDiffVersion) { + lastDiffVersion = diffVersion + if (userHasEdits) { + state = "detected" + return true + } + } + return false + }, + dismiss() { + state = "none" + }, + reload() { + state = "none" + }, + } +} + +describe("Concurrent edit detection", () => { + test("starts in none state", () => { + const det = createConcurrentEditDetector() + expect(det.state).toBe("none") + }) + + test("first diff update sets baseline, no detection", () => { + const det = createConcurrentEditDetector() + const detected = det.onDiffUpdate("v1") + expect(detected).toBe(false) + expect(det.state).toBe("none") + }) + + test("same diff version = no detection", () => { + const det = createConcurrentEditDetector() + det.onDiffUpdate("v1") + det.setUserHasEdits(true) + const detected = det.onDiffUpdate("v1") + expect(detected).toBe(false) + }) + + test("new diff version with user edits = detected", () => { + const det = createConcurrentEditDetector() + det.onDiffUpdate("v1") + det.setUserHasEdits(true) + const detected = det.onDiffUpdate("v2") + expect(detected).toBe(true) + expect(det.state).toBe("detected") + }) + + test("new diff version without user edits = silent update", () => { + const det = createConcurrentEditDetector() + det.onDiffUpdate("v1") + det.setUserHasEdits(false) + const detected = det.onDiffUpdate("v2") + expect(detected).toBe(false) + expect(det.state).toBe("none") + }) + + test("dismiss clears detected state", () => { + const det = createConcurrentEditDetector() + det.onDiffUpdate("v1") + det.setUserHasEdits(true) + det.onDiffUpdate("v2") + expect(det.state).toBe("detected") + + det.dismiss() + expect(det.state).toBe("none") + }) + + test("reload clears detected state", () => { + const det = createConcurrentEditDetector() + det.onDiffUpdate("v1") + det.setUserHasEdits(true) + det.onDiffUpdate("v2") + + det.reload() + expect(det.state).toBe("none") + }) +}) diff --git a/packages/extension/test/sidebar_view.test.ts b/packages/extension/test/sidebar_view.test.ts index 1cbbad36..419c391e 100644 --- a/packages/extension/test/sidebar_view.test.ts +++ b/packages/extension/test/sidebar_view.test.ts @@ -1238,6 +1238,19 @@ describe("sidebar webview — context menu", () => { expect(src).toContain("Open in Terminal"); }); + it("context menu shows 'Restore' for ghost (deleted) entries instead of normal file ops", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // Reads gitStatus from the data attribute to detect ghost entries + expect(src).toMatch(/dataset\.gitStatus\s*===\s*"deleted"/); + // Shows "Restore" for ghost entries + expect(src).toContain("Restore"); + // The restore op + expect(src).toContain('"restore"'); + }); + it("sidebar_view.ts CSS includes context-menu styling", () => { const provider = new SidebarViewProvider(makeExtensionUri()); const view = makeWebviewView(); @@ -1634,6 +1647,15 @@ describe("sidebar webview — drag and drop", () => { expect(req.op).toBe("move"); expect(req.targetDir).toBe("/lib"); }); + + it("FileOpRequest type includes restore op for git-deleted ghost entries", async () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_bridge.ts"), + "utf8", + ); + // The op union should include "restore" + expect(src).toMatch(/"restore"/); + }); }); // ── Git status colors (#673) ───────────────────────────────────────────────── @@ -1669,6 +1691,20 @@ describe("sidebar — git status colors", () => { expect(html).toContain("--vscode-gitDecoration-untrackedResourceForeground"); }); + it("CSS suppresses strikethrough on directory nodes with git-deleted status", async () => { + const { SidebarViewProvider } = await import("../src/sidebar_view"); + const provider = new SidebarViewProvider(makeExtensionUri()); + const view = makeWebviewView(); + provider.resolveWebviewView(view, {}, { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) }); + const html = view.webview.html; + + // Deleted files get strikethrough + expect(html).toContain(".git-deleted"); + expect(html).toMatch(/\.git-deleted\s*\{[^}]*text-decoration:\s*line-through/); + // But directories do NOT — the override rule suppresses it + expect(html).toMatch(/\[data-type="directory"\].*\.git-deleted\s*\{[^}]*text-decoration:\s*none/); + }); + it("webview applies git status CSS classes to file labels", () => { const src = readFileSync( resolve(__dirname, "..", "src", "sidebar_webview.ts"), @@ -1679,6 +1715,17 @@ describe("sidebar — git status colors", () => { expect(src).toMatch(/label\.classList\.add.*git-/); }); + it("renderFileNode stores gitStatus as a data attribute for context menu detection", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // The file node's DOM element should carry data-git-status when gitStatus is set + expect(src).toContain("dataset.gitStatus"); + // Should be set from entry.gitStatus + expect(src).toMatch(/dataset\.gitStatus\s*=\s*entry\.gitStatus/); + }); + it("sidebar_view.ts annotates children with git status from the Git extension", () => { const src = readFileSync( resolve(__dirname, "..", "src", "sidebar_view.ts"), @@ -1692,6 +1739,19 @@ describe("sidebar — git status colors", () => { // Walks workingTreeChanges and indexChanges expect(src).toContain("workingTreeChanges"); expect(src).toContain("indexChanges"); + // Injects ghost entries for deleted files before annotation + expect(src).toContain("injectDeletedEntries"); + }); + + it("annotateGitStatus receives the parent directory path so it can inject deleted ghost entries", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_view.ts"), + "utf8", + ); + // annotateGitStatus should accept a parentDir parameter + expect(src).toMatch(/annotateGitStatus\(entries,\s*\w+/); + // The getChildren call site should pass the directory path + expect(src).toMatch(/annotateGitStatus\(entries,\s*p\)/); }); it("propagateGitStatusToDirs gives directories the most notable child status", async () => { @@ -1702,6 +1762,7 @@ describe("sidebar — git status colors", () => { { name: "src", type: "directory" as const, path: "/project/src" }, { name: "docs", type: "directory" as const, path: "/project/docs" }, { name: "clean", type: "directory" as const, path: "/project/clean" }, + { name: "trash", type: "directory" as const, path: "/project/trash" }, { name: "main.ts", type: "file" as const, path: "/project/main.ts", gitStatus: "modified" as const }, ]; @@ -1710,6 +1771,7 @@ describe("sidebar — git status colors", () => { ["/project/src/index.ts", "modified"], ["/project/src/util.ts", "untracked"], ["/project/docs/README.md", "added"], + ["/project/trash/old.ts", "deleted"], ]); const result = propagateGitStatusToDirs(entries, statusMap); @@ -1720,9 +1782,65 @@ describe("sidebar — git status colors", () => { expect(result.find(e => e.name === "docs")?.gitStatus).toBe("added"); // clean has no changed children → no git status expect(result.find(e => e.name === "clean")?.gitStatus).toBeUndefined(); - // Files keep their original status + // trash has deleted children → gets "deleted" color (CSS prevents strikethrough on dirs) + expect(result.find(e => e.name === "trash")?.gitStatus).toBe("deleted"); + // Files keep their original status expect(result.find(e => e.name === "main.ts")?.gitStatus).toBe("modified"); }); + + it("injectDeletedEntries adds ghost entries for deleted files missing from the directory listing", async () => { + vi.resetModules(); + const { injectDeletedEntries } = await import("../src/sidebar_view"); + + // Existing entries from the filesystem — deleted.ts is NOT here (already removed from disk) + const entries = [ + { name: "main.ts", type: "file" as const, path: "/project/src/main.ts" }, + { name: "util.ts", type: "file" as const, path: "/project/src/util.ts", gitStatus: "modified" as const }, + ]; + + // Git status map includes a deleted file under the same directory + const statusMap = new Map([ + ["/project/src/deleted.ts", "deleted"], + ["/project/src/util.ts", "modified"], + ]); + + const result = injectDeletedEntries(entries, statusMap, "/project/src"); + + // The ghost entry should appear in the result + const ghost = result.find(e => e.name === "deleted.ts"); + expect(ghost).toBeDefined(); + expect(ghost!.path).toBe("/project/src/deleted.ts"); + expect(ghost!.type).toBe("file"); + expect(ghost!.gitStatus).toBe("deleted"); + + // Existing entries should still be present and unchanged + expect(result.find(e => e.name === "main.ts")).toBeDefined(); + expect(result.find(e => e.name === "util.ts")?.gitStatus).toBe("modified"); + + // No duplicate: util.ts exists on disk, so it shouldn't be injected again + expect(result.filter(e => e.name === "util.ts")).toHaveLength(1); + }); + + it("injectDeletedEntries does not inject files from subdirectories (only direct children)", async () => { + vi.resetModules(); + const { injectDeletedEntries } = await import("../src/sidebar_view"); + + const entries = [ + { name: "index.ts", type: "file" as const, path: "/project/src/index.ts" }, + ]; + + const statusMap = new Map([ + ["/project/src/deleted.ts", "deleted"], // direct child — should appear + ["/project/src/sub/deep.ts", "deleted"], // nested — should NOT appear + ["/project/other/file.ts", "deleted"], // different dir — should NOT appear + ]); + + const result = injectDeletedEntries(entries, statusMap, "/project/src"); + + expect(result.find(e => e.name === "deleted.ts")).toBeDefined(); + expect(result.find(e => e.name === "deep.ts")).toBeUndefined(); + expect(result.find(e => e.name === "file.ts")).toBeUndefined(); + }); }); // ── Reactive git status (#673 — push git changes to webview) ───────────────── @@ -1906,6 +2024,19 @@ describe("sidebar — reactive git status", () => { expect(src).toMatch(/classList\.remove|className.*replace|git-/); }); + it("applyGitStatus updates dataset.gitStatus on file nodes so click/context-menu guards stay current", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // applyGitStatus should set dataset.gitStatus when a status is present + // and delete it when the status is cleared (file restored) + const applyFn = src.slice(src.indexOf("function applyGitStatus"), src.indexOf("function applyGitStatus") + 1500); + expect(applyFn).toContain("dataset.gitStatus"); + // Should clear it when no status matches (delete operator or empty assignment) + expect(applyFn).toMatch(/delete\s+.*dataset\.gitStatus|dataset\.gitStatus\s*=\s*""/); + }); + it("webview applies git status to root-level project nodes too", () => { const src = readFileSync( resolve(__dirname, "..", "src", "sidebar_webview.ts"), @@ -1918,6 +2049,20 @@ describe("sidebar — reactive git status", () => { expect(src).toContain("startsWith"); }); + it("webview re-requests children when git-status has orphaned deleted entries with no DOM node", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // The git-status handler should detect orphaned deleted entries + // (deleted files in the status map with no matching DOM node) + // and re-request children for the parent directory so ghost entries appear. + // Look for the re-request pattern in the git-status case: + expect(src).toContain("orphaned"); + // Should invalidate cache and re-request children + expect(src).toMatch(/get-children/); + }); + it("host pushes git-status after every roots re-render to prevent color loss", async () => { vi.resetModules(); @@ -2610,3 +2755,354 @@ describe("executeFileOp — delete with confirmation", () => { (vs.workspace as any).workspaceFolders = []; }); }); + +// ── Restore (git checkout) for ghost entries ───────────────────────────────── + +describe("executeFileOp — restore ghost entry", () => { + it("executeFileOp restore case runs git checkout HEAD to restore a deleted file", async () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_view.ts"), + "utf8", + ); + // Should have a "restore" case in executeFileOp + expect(src).toMatch(/case\s*"restore"/); + // Should run git checkout HEAD -- + expect(src).toContain("git"); + expect(src).toContain("checkout"); + expect(src).toContain("HEAD"); + }); +}); + +// ── Ghost entry click suppression ──────────────────────────────────────────── + +describe("ghost entry click behavior", () => { + it("single-clicking a ghost (deleted) file does not post open-file", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // The click handler in renderFileNode should check gitStatus before posting open-file + // Find the click handler block near "open-file" + const openFileIdx = src.indexOf('"open-file"'); + expect(openFileIdx).toBeGreaterThan(-1); + // There should be a gitStatus guard before the open-file post + const blockBefore = src.slice(Math.max(0, openFileIdx - 200), openFileIdx); + expect(blockBefore).toContain("gitStatus"); + }); + + it("click guard reads dataset.gitStatus from the DOM at click time, not from a closure", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // Find the click handler near "open-file" + const openFileIdx = src.indexOf('"open-file"'); + const blockBefore = src.slice(Math.max(0, openFileIdx - 300), openFileIdx); + // Must read from row.dataset.gitStatus (DOM), not entry.gitStatus (closure) + expect(blockBefore).toContain("row.dataset.gitStatus"); + expect(blockBefore).not.toContain("entry.gitStatus"); + }); +}); + +// ── Root-reorder drop handler: non-root files must pass through ────────────── + +describe("root-reorder drop handler propagation", () => { + it("does NOT call stopImmediatePropagation before confirming the source is a root", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // Find the setupRootReorderDropTarget function's drop handler + const fnStart = src.indexOf("function setupRootReorderDropTarget"); + expect(fnStart).toBeGreaterThan(-1); + const fnBody = src.slice(fnStart, fnStart + 3500); + + // Find the drop listener within this function (skip "dragleave"/"dragover") + const dropIdx = fnBody.indexOf('"drop"'); + expect(dropIdx).toBeGreaterThan(-1); + const dropBody = fnBody.slice(dropIdx, dropIdx + 600); + + // stopImmediatePropagation must come AFTER the sourceRoot check, + // not before it — otherwise non-root file drops are silently swallowed + const stopIdx = dropBody.indexOf("stopImmediatePropagation"); + const rootCheckIdx = dropBody.indexOf("currentRoots.find"); + expect(stopIdx).toBeGreaterThan(-1); + expect(rootCheckIdx).toBeGreaterThan(-1); + expect(rootCheckIdx).toBeLessThan(stopIdx); + }); +}); + +// ── Lane 2 relay allowlist for file-op-notify ──────────────────────────────── + +describe("chat panel Lane 2 relay", () => { + it("allowlists file-op-notify in both relay scripts so sidebar file moves reach the session page", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "chat_panel.ts"), + "utf8", + ); + // There are two relay scripts (renderHtml and renderTransitionHtml), + // each must include file-op-notify in their allowlist + const matches = src.match(/file-op-notify/g); + expect(matches).toBeDefined(); + expect(matches!.length).toBeGreaterThanOrEqual(2); + }); +}); + +// ── File-op-notify: sidebar notifies chat panel after move/rename ───────────── + +describe("file-op-notify for Files Changed tracking", () => { + it("executeFileOp returns newPath for move operations", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_view.ts"), + "utf8", + ); + // Find the move case in executeFileOp + const moveCase = src.indexOf('case "move"'); + expect(moveCase).toBeGreaterThan(-1); + const moveBlock = src.slice(moveCase, moveCase + 800); + // Must return newPath so the bridge can notify the session page + expect(moveBlock).toContain("newPath"); + }); + + it("executeFileOp returns newPath for rename operations", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_view.ts"), + "utf8", + ); + // Find the rename case in executeFileOp + const renameCase = src.indexOf('case "rename"'); + expect(renameCase).toBeGreaterThan(-1); + const renameBlock = src.slice(renameCase, renameCase + 1000); + expect(renameBlock).toContain("newPath"); + }); + + it("sidebar_bridge posts file-op-notify to the chat panel after a successful move/rename", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_bridge.ts"), + "utf8", + ); + expect(src).toContain("file-op-notify"); + expect(src).toContain("oldPath"); + expect(src).toContain("newPath"); + }); + + it("notifyFileMove sends os.homedir() so the browser iframe can normalize paths", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_view.ts"), + "utf8", + ); + const notifyBlock = src.slice(src.indexOf("notifyFileMove"), src.indexOf("notifyFileMove") + 400); + expect(notifyBlock).toContain("homedir"); + }); +}); + +// ── Drag auto-scroll ───────────────────────────────────────────────────────── + +describe("drag auto-scroll", () => { + it("all three dragover handlers call handleDragAutoScroll", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // Each of the three setup functions should call handleDragAutoScroll in their dragover + for (const fn of ["setupDirectoryDropTarget", "setupFileDropTarget", "setupRootReorderDropTarget"]) { + const fnStart = src.indexOf(`function ${fn}`); + expect(fnStart, `${fn} exists`).toBeGreaterThan(-1); + const fnBody = src.slice(fnStart, fnStart + 2500); + expect(fnBody, `${fn} calls handleDragAutoScroll`).toContain("handleDragAutoScroll"); + } + }); + + it("clearAutoScroll is called on dragend and in clearDropTarget", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // dragend listener should clear auto-scroll + const dragendIdx = src.indexOf('"dragend"'); + expect(dragendIdx).toBeGreaterThan(-1); + const dragendBlock = src.slice(dragendIdx, dragendIdx + 300); + expect(dragendBlock).toContain("clearAutoScroll"); + + // clearDropTarget should also clear auto-scroll + const clearDropIdx = src.indexOf("function clearDropTarget"); + expect(clearDropIdx).toBeGreaterThan(-1); + const clearDropBlock = src.slice(clearDropIdx, clearDropIdx + 300); + expect(clearDropBlock).toContain("clearAutoScroll"); + }); +}); + +// ── Sidebar flicker prevention ─────────────────────────────────────────────── + +describe("sidebar — flicker prevention", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("filesystem watcher debounces rapid events into a single fs-changed per folder", async () => { + vi.useFakeTimers(); + vi.resetModules(); + + // Wire a mock createFileSystemWatcher that captures event handlers + const fsCbs: { create: Array<(uri: any) => void>; change: Array<(uri: any) => void>; del: Array<(uri: any) => void> } = { + create: [], change: [], del: [], + }; + const vscodeMock = await import("vscode"); + (vscodeMock.workspace as any).createFileSystemWatcher = () => ({ + onDidCreate: (cb: (uri: any) => void) => { fsCbs.create.push(cb); return { dispose() {} }; }, + onDidChange: (cb: (uri: any) => void) => { fsCbs.change.push(cb); return { dispose() {} }; }, + onDidDelete: (cb: (uri: any) => void) => { fsCbs.del.push(cb); return { dispose() {} }; }, + dispose() {}, + }); + // Set up a workspace folder so getWorkspaceFolder resolves + (vscodeMock.workspace as any).workspaceFolders = [ + { uri: { fsPath: "/project" }, name: "project", index: 0 }, + ]; + + const { SidebarViewProvider } = await import("../src/sidebar_view"); + const provider = new SidebarViewProvider(makeExtensionUri()); + const view = makeWebviewView(); + provider.resolveWebviewView(view, {}, { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) }); + (view.webview.postMessage as any).mockClear(); + + // Fire 10 rapid file-change events (simulating a git checkout) + for (let i = 0; i < 10; i++) { + for (const cb of fsCbs.change) cb({ fsPath: `/project/src/file${i}.ts` }); + } + + // Within the debounce window, no fs-changed should have been sent + const callsBefore = (view.webview.postMessage as any).mock.calls + .filter((c: any[]) => c[0]?.kind === "fs-changed"); + expect(callsBefore).toHaveLength(0); + + // Advance past the debounce window + vi.advanceTimersByTime(350); + + // Exactly one fs-changed for the /project folder + const callsAfter = (view.webview.postMessage as any).mock.calls + .filter((c: any[]) => c[0]?.kind === "fs-changed"); + expect(callsAfter).toHaveLength(1); + expect(callsAfter[0][0].folder).toBe("/project"); + + // Restore + (vscodeMock.workspace as any).workspaceFolders = []; + }); + + it("filesystem watcher coalesces events from multiple folders", async () => { + vi.useFakeTimers(); + vi.resetModules(); + + const fsCbs: { create: Array<(uri: any) => void>; change: Array<(uri: any) => void>; del: Array<(uri: any) => void> } = { + create: [], change: [], del: [], + }; + const vscodeMock = await import("vscode"); + (vscodeMock.workspace as any).createFileSystemWatcher = () => ({ + onDidCreate: (cb: (uri: any) => void) => { fsCbs.create.push(cb); return { dispose() {} }; }, + onDidChange: (cb: (uri: any) => void) => { fsCbs.change.push(cb); return { dispose() {} }; }, + onDidDelete: (cb: (uri: any) => void) => { fsCbs.del.push(cb); return { dispose() {} }; }, + dispose() {}, + }); + (vscodeMock.workspace as any).workspaceFolders = [ + { uri: { fsPath: "/project-a" }, name: "a", index: 0 }, + { uri: { fsPath: "/project-b" }, name: "b", index: 1 }, + ]; + + const { SidebarViewProvider } = await import("../src/sidebar_view"); + const provider = new SidebarViewProvider(makeExtensionUri()); + const view = makeWebviewView(); + provider.resolveWebviewView(view, {}, { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) }); + (view.webview.postMessage as any).mockClear(); + + // Interleave events from two folders + for (const cb of fsCbs.change) { + cb({ fsPath: "/project-a/file1.ts" }); + cb({ fsPath: "/project-b/file1.ts" }); + cb({ fsPath: "/project-a/file2.ts" }); + } + + vi.advanceTimersByTime(350); + + const fsMsgs = (view.webview.postMessage as any).mock.calls + .filter((c: any[]) => c[0]?.kind === "fs-changed"); + // One message per folder, not per event + expect(fsMsgs).toHaveLength(2); + const folders = fsMsgs.map((c: any[]) => c[0].folder).sort(); + expect(folders).toEqual(["/project-a", "/project-b"]); + + (vscodeMock.workspace as any).workspaceFolders = []; + }); + + it("dispose clears the filesystem debounce timer so it does not fire into a dead webview", async () => { + vi.useFakeTimers(); + vi.resetModules(); + + const fsCbs: { change: Array<(uri: any) => void> } = { change: [] }; + const vscodeMock = await import("vscode"); + (vscodeMock.workspace as any).createFileSystemWatcher = () => ({ + onDidCreate: () => ({ dispose() {} }), + onDidChange: (cb: (uri: any) => void) => { fsCbs.change.push(cb); return { dispose() {} }; }, + onDidDelete: () => ({ dispose() {} }), + dispose() {}, + }); + (vscodeMock.workspace as any).workspaceFolders = [ + { uri: { fsPath: "/project" }, name: "project", index: 0 }, + ]; + + const { SidebarViewProvider } = await import("../src/sidebar_view"); + const provider = new SidebarViewProvider(makeExtensionUri()); + const view = makeWebviewView(); + provider.resolveWebviewView(view, {}, { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) }); + (view.webview.postMessage as any).mockClear(); + + // Fire an event, starting the debounce timer + for (const cb of fsCbs.change) cb({ fsPath: "/project/file.ts" }); + + // Dispose before the timer fires + for (const cb of view._disposeCbs) cb(); + + // Advance past the debounce window + vi.advanceTimersByTime(350); + + // The fs-changed message should NOT have been sent (timer was cleared) + const fsMsgs = (view.webview.postMessage as any).mock.calls + .filter((c: any[]) => c[0]?.kind === "fs-changed"); + expect(fsMsgs).toHaveLength(0); + + (vscodeMock.workspace as any).workspaceFolders = []; + }); + + it("section-order handler skips re-render when order is unchanged", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "sidebar_webview.ts"), + "utf8", + ); + // The section-order message handler should compare incoming order with + // currentSectionOrder before calling renderRoots, to avoid a redundant + // full DOM wipe on sidebar show and section-reorder echo. + const handlerStart = src.indexOf('case "section-order"'); + expect(handlerStart).toBeGreaterThan(-1); + const handlerBlock = src.slice(handlerStart, handlerStart + 800); + // Must check equality before rendering — look for a comparison guard + expect(handlerBlock).toMatch(/currentSectionOrder/); + // The guard must come BEFORE renderRoots — find both positions + const guardIdx = handlerBlock.indexOf("currentSectionOrder"); + const renderIdx = handlerBlock.indexOf("renderRoots"); + expect(renderIdx).toBeGreaterThan(-1); + // There must be an early-exit/skip before the renderRoots call + expect(handlerBlock).toMatch(/JSON\.stringify|every|===.*break|return/); + }); + + it("sidebar webview is registered with retainContextWhenHidden", () => { + const src = readFileSync( + resolve(__dirname, "..", "src", "extension.ts"), + "utf8", + ); + // Find the registerWebviewViewProvider call for the sidebar + const regIdx = src.indexOf('registerWebviewViewProvider("amicode.workspace"'); + expect(regIdx).toBeGreaterThan(-1); + // The third argument should include retainContextWhenHidden: true + const regBlock = src.slice(regIdx, regIdx + 200); + expect(regBlock).toContain("retainContextWhenHidden"); + expect(regBlock).toContain("true"); + }); +});