diff --git a/package-lock.json b/package-lock.json index 0175134c..bdd19b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twinny", - "version": "3.19.6", + "version": "3.19.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twinny", - "version": "3.19.6", + "version": "3.19.7", "cpu": [ "x64", "arm64" diff --git a/package.json b/package.json index 49e0e29a..488412fe 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "twinny", "displayName": "twinny - AI Code Completion and Chat", "description": "Locally hosted AI code completion plugin for vscode", - "version": "3.19.6", + "version": "3.19.7", "icon": "assets/icon.png", "keywords": [ "code-inference", @@ -286,35 +286,51 @@ }, "markdownDescription": "Specifies which languages to enable completions with Twinny for. Use `*` as the default for all languages. Example:\n```json\n{\n \"*\": true,\n \"python\": true,\n \"javascript\": false\n}\n```" }, - "twinny.autoSuggestEnabled": { + "twinny.locale": { "order": 2, + "type": "string", + "enum": [ + "en", + "zh-CN", + "zh-HK" + ], + "enumDescriptions": [ + "English", + "Chinese (Simplified)", + "Chinese (Hong Kong)" + ], + "default": "en", + "markdownDescription": "Sets the locale for Twinny. The default is `en` (English)." + }, + "twinny.autoSuggestEnabled": { + "order": 3, "type": "boolean", "default": true, "markdownDescription": "When `true`, Twinny will automatically suggest completions. You can still manually trigger completions using the default shortcut (`Alt+\\`)." }, "twinny.contextLength": { - "order": 3, + "order": 4, "type": "number", "default": 100, "markdownDescription": "Specifies how many lines of context (before and after the current line) to include in Fill-in-Middle (FIM) prompts. A higher number provides more context but may slow down completions.", "required": true }, "twinny.debounceWait": { - "order": 4, + "order": 5, "type": "number", "default": 300, "markdownDescription": "Sets the delay (in milliseconds) before triggering the next completion. This helps reduce API calls and improve performance.", "required": true }, "twinny.temperature": { - "order": 5, + "order": 6, "type": "number", "default": 0.2, "markdownDescription": "Controls the randomness of the model's output. Lower values (e.g., 0.2) produce more focused and deterministic outputs, while higher values (e.g., 0.8) lead to more diverse and creative completions.", "required": true }, "twinny.multilineCompletionsEnabled": { - "order": 6, + "order": 7, "type": "boolean", "default": true, "markdownDescription": "When `true`, Twinny will attempt to generate multi-line completions. This is an experimental feature and may not work perfectly in all scenarios." @@ -323,7 +339,7 @@ "dependencies": { "twinny.multilineCompletionsEnabled": true }, - "order": 7, + "order": 8, "type": "number", "default": 30, "markdownDescription": "Sets the maximum number of lines for multi-line completions. Only applies when `twinny.multilineCompletionsEnabled` is `true`." diff --git a/src/common/constants.ts b/src/common/constants.ts index 81606c98..fbdac2b5 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -34,6 +34,7 @@ export const defaultChunkOptions = { } export const EVENT_NAME = { + twinntGetLocale: "twinnt-get-locale", twinnyAcceptSolution: "twinny-accept-solution", twinnyAddMessage: "twinny-add-message", twinnyChat: "twinny-chat", @@ -43,6 +44,7 @@ export const EVENT_NAME = { twinnyConnectSymmetry: "twinny-connect-symmetry", twinnyDisconnectedFromSymmetry: "twinny-disconnected-from-symmetry", twinnyDisconnectSymmetry: "twinny-disconnect-symmetry", + twinnyEditDefaultTemplates: "twinny-edit-default-templates", twinnyEmbedDocuments: "twinny-embed-documents", twinnyEnableModelDownload: "twinny-enable-model-download", twinnyFetchOllamaModels: "twinny-fetch-ollama-models", @@ -72,11 +74,11 @@ export const EVENT_NAME = { twinnySessionContext: "twinny-session-context", twinnySetConfigValue: "twinny-set-config-value", twinnySetGlobalContext: "twinny-set-global-context", + twinnySetLocale: "twinny-set-locale", twinnySetOllamaModel: "twinny-set-ollama-model", twinnySetSessionContext: "twinny-set-session-context", twinnySetTab: "twinny-set-tab", twinnySetWorkspaceContext: "twinny-set-workspace-context", - twinnyEditDefaultTemplates: "twinny-edit-default-templates", twinnyStartSymmetryProvider: "twinny-start-symmetry-provider", twinnyStopGeneration: "twinny-stop-generation", twinnyStopSymmetryProvider: "twinny-stop-symmetry-provider", diff --git a/src/extension/api.ts b/src/extension/api.ts index c49d230f..5ba242ca 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -16,14 +16,8 @@ export async function streamResponse(request: StreamRequest) { const { signal } = controller const timeOut = setTimeout(() => { - controller.abort() - onError?.(new Error("Request timed out")) - log.logConsoleError( - Logger.ErrorType.Timeout, - "Failed to establish connection", - new Error("Request timed out") - ) - }, 25000) + controller.abort(new DOMException("Request timed out", "TimeoutError")) + }, 60000) try { const url = `${options.protocol}://${options.hostname}${ @@ -102,6 +96,9 @@ export async function streamResponse(request: StreamRequest) { if (error instanceof Error) { if (error.name === "AbortError") { onEnd?.() + } else if (error.name === "TimeoutError") { + onError?.(error) + log.logConsoleError(Logger.ErrorType.Timeout, "Failed to establish connection", error) } else { log.logConsoleError(Logger.ErrorType.Fetch_Error, "Fetch error", error) onError?.(error) diff --git a/src/extension/chat-service.ts b/src/extension/chat-service.ts index f4f8d710..8a77ddaa 100644 --- a/src/extension/chat-service.ts +++ b/src/extension/chat-service.ts @@ -530,7 +530,7 @@ export class ChatService extends Base { return combinedContext.trim() || null } - private async loadFileContents(files: FileItem[]): Promise { + private async loadFileContents(files?: FileItem[]): Promise { if (!files?.length) return "" let fileContents = "" for (const file of files) { @@ -546,7 +546,7 @@ export class ChatService extends Base { public async streamChatCompletion( messages: Message[], - filePaths: FileItem[] + filePaths?: FileItem[] ) { this._completion = "" this.sendEditorLanguage() @@ -577,7 +577,7 @@ export class ChatService extends Base { additionalContext += `Additional Context:\n${ragContext}\n\n` } - filePaths = filePaths.filter((filepath) => + filePaths = filePaths?.filter((filepath) => filepath.name !== "workspace" && filepath.name !== "problems" ) const fileContents = await this.loadFileContents(filePaths) diff --git a/src/extension/providers/base.ts b/src/extension/providers/base.ts index c0070d71..735397fc 100644 --- a/src/extension/providers/base.ts +++ b/src/extension/providers/base.ts @@ -143,12 +143,24 @@ export class BaseProvider { [EVENT_NAME.twinnyFileListRequest]: this.fileListRequest, [EVENT_NAME.twinnyNewConversation]: this.twinnyNewConversation, [EVENT_NAME.twinnyEditDefaultTemplates]: this.editDefaultTemplates, + [EVENT_NAME.twinntGetLocale]: this.sendLocaleToWebView, [TWINNY_COMMAND_NAME.settings]: this.openSettings } this.webView?.onDidReceiveMessage((message: ClientMessage) => { const eventHandler = eventHandlers[message.type as string] if (eventHandler) eventHandler(message) }) + vscode.workspace.onDidChangeConfiguration((event) => { + if (!event.affectsConfiguration("twinny")) return + this.sendLocaleToWebView() + }) + } + + private sendLocaleToWebView = () => { + this.webView?.postMessage({ + type: EVENT_NAME.twinnySetLocale, + data: vscode.workspace.getConfiguration("twinny").get("locale") as string + }) } private handleThemeChange = () => { diff --git a/src/extension/review-service.ts b/src/extension/review-service.ts index 32ac9e0d..060ea3ad 100644 --- a/src/extension/review-service.ts +++ b/src/extension/review-service.ts @@ -26,6 +26,7 @@ import { getChatDataFromProvider, updateLoadingMessage } from "./utils" export class GithubService extends ConversationHistory { private _completion = "" private _templateProvider: TemplateProvider + private _controller?: AbortController constructor( context: ExtensionContext, @@ -190,6 +191,14 @@ export class GithubService extends ConversationHistory { return streamResponse({ body: requestBody, options: requestOptions, + onStart: (controller: AbortController) => { + this._controller = controller + this.webView?.onDidReceiveMessage((data: { type: string }) => { + if (data.type === EVENT_NAME.twinnyStopGeneration) { + this._controller?.abort() + } + }) + }, onData: (streamResponse) => { const provider = this.getProvider() if (!provider) return diff --git a/src/webview/assets/locales/zh-CN.json b/src/webview/assets/locales/zh-CN.json new file mode 100644 index 00000000..eba722d8 --- /dev/null +++ b/src/webview/assets/locales/zh-CN.json @@ -0,0 +1,91 @@ +{ + "accept-solution": "接受解决方案", + "api-key-placeholder": "在这里输入您的API密钥", + "api-key": "API密钥", + "api-path-placeholder": "输入主机名,例如 'localhost'", + "api-path": "API路径", + "applicable-ollama": "适用于Ollama等一些接口提供者", + "auto-connect-as-provider": "作为接口提供者自动连接", + "automatic": "自动化", + "cancel-edit": "取消编辑", + "cancel": "取消", + "chat": "聊天", + "clear-conversations": "清除对话", + "connect": "连接", + "connected": "已连接!", + "connecting": "正在连接...", + "connection-failed": "连接失败!请检查您的连接后重试。", + "consumer-connection": "用户连接", + "conversation-history": "对话历史", + "copy-code": "复制代码", + "copy-provider": "复制接口提供者", + "delete-message": "删除消息", + "delete-provider": "删除接口提供者", + "disconnect": "断开连接", + "edit-default-templates-description": "编辑在Twinny扩展中使用的默认模板。", + "edit-default-templates": "编辑默认模板", + "edit-message": "编辑消息", + "edit-provider": "编辑接口提供者", + "embed-documents": "嵌入文档", + "embedding-provider": "嵌入接口提供者", + "fim-template": "FIM模板", + "fim": "中间填充", + "hostname-placeholder": "输入主机名,例如 'localhost'", + "hostname": "主机名", + "label-placeholder": "为您的接口提供者输入一个标签。", + "label": "标签", + "loading-available-models": "正在加载可用模型...", + "max-chunk-size": "最大块大小", + "min-chunk-size": "最小块大小", + "model-name-placeholder": "输入模型名称,例如 'llama3'", + "model-name": "模型名称", + "new-conversation": "新对话", + "new-document": "新文档", + "no-connections-found": "未找到连接。请添加新连接以开始。", + "no-result": "无结果", + "nothing-to-see-here": "这里没什么可看的。", + "number-code-filepaths": "用作上下文的文件路径数量。", + "number-code-snippets": "用作上下文的代码片段数量。", + "open-diff": "打开差异比较视图", + "open-template-editor": "打开模板编辑器", + "overlap-size": "重叠大小", + "owner-repo-name": "此标签将帮助您审查您的存储库中的拉取请求,输入下面的所有者和存储库名称以开始。目前仅支持GitHub,请在设置标签中设置您的GitHub令牌以开始。", + "path": "路径", + "placeholder": "Twinny今天能怎么帮助您?", + "port-placeholder": "输入端口号,例如 '11434'", + "port": "端口", + "protocol": "协议", + "provider-connection": "接口提供者连接", + "provider-name": "接口提供者名称", + "provider-placeholder": "输入接口提供者名称", + "provider-type": "接口提供者类型", + "provider": "接口提供者", + "providers": "接口提供者们", + "pull-requests": "拉取请求", + "regenerate-message": "重新生成消息", + "relevant-code-snippets": "相关代码片段", + "relevant-file-paths": "相关文件路径", + "repository-level": "存储库级别", + "rerank-probability-threshold": "重新排名概率阈值", + "rerank-threshold-description": "阈值越低,结果越有可能被包含。", + "rerank-threshold": "重新排名阈值", + "reset-providers": "重置接口提供者", + "reset-to-default": "重置为默认", + "review-pull-requests": "审查拉取请求", + "save-edit": "保存编辑", + "save": "保存", + "scroll-down": "滚动到底部", + "share-gpu-resources": "您也可以通过使用您的活跃Twinny接口提供者配置作为接口提供者连接到Symmetry来共享您的GPU资源。所有连接都是点对点的,端到端加密且安全的。", + "status": "状态", + "stop-generation": "停止生成", + "symmetry-description": "Symmetry是一个点对点AI推理网络,允许用户之间进行安全、直接的连接。当您作为消费者连接时,Symmetry会根据您的模型选择为您匹配接口提供者。", + "symmetry-inference-network": "Symmetry推理网络", + "template-settings-description": "选择您想要在聊天界面中使用的模板。", + "template-settings": "模板设置", + "thinking": "思考中...", + "toggle-auto-scroll": "开启/关闭自动滚动", + "toggle-embedding-options": "开启/关闭嵌入选项", + "toggle-provider-selection": "开启/关闭接口提供者选择", + "type": "类型" +} + \ No newline at end of file diff --git a/src/webview/assets/locales/zh-HK.json b/src/webview/assets/locales/zh-HK.json new file mode 100644 index 00000000..9b94da14 --- /dev/null +++ b/src/webview/assets/locales/zh-HK.json @@ -0,0 +1,91 @@ +{ + "accept-solution": "接受解決方案", + "api-key-placeholder": "在這裡輸入您的API密鑰", + "api-key": "API密鑰", + "api-path-placeholder": "輸入主機名,例如 'localhost'", + "api-path": "API路徑", + "applicable-ollama": "適用於Ollama等一些接口提供者", + "auto-connect-as-provider": "作為接口提供者自動連接", + "automatic": "自動化", + "cancel-edit": "取消編輯", + "cancel": "取消", + "chat": "聊天", + "clear-conversations": "清除對話", + "connect": "連接", + "connected": "已連接!", + "connecting": "正在連接...", + "connection-failed": "連接失敗!請檢查您的連接後重試。", + "consumer-connection": "用戶連接", + "conversation-history": "對話歷史", + "copy-code": "複製代碼", + "copy-provider": "複製接口提供者", + "delete-message": "刪除消息", + "delete-provider": "刪除接口提供者", + "disconnect": "斷開連接", + "edit-default-templates-description": "編輯在Twinny擴展中使用的默認模板。", + "edit-default-templates": "編輯默認模板", + "edit-message": "編輯消息", + "edit-provider": "編輯接口提供者", + "embed-documents": "嵌入文檔", + "embedding-provider": "嵌入接口提供者", + "fim-template": "FIM模板", + "fim": "中間填充", + "hostname-placeholder": "輸入主機名,例如 'localhost'", + "hostname": "主機名", + "label-placeholder": "為您的接口提供者輸入一個標籤。", + "label": "標籤", + "loading-available-models": "正在加載可用模型...", + "max-chunk-size": "最大塊大小", + "min-chunk-size": "最小塊大小", + "model-name-placeholder": "輸入模型名稱,例如 'llama3'", + "model-name": "模型名稱", + "new-conversation": "新對話", + "new-document": "新文檔", + "no-connections-found": "未找到連接。請添加新連接以開始。", + "no-result": "無結果", + "nothing-to-see-here": "這裡沒什麼可看的。", + "number-code-filepaths": "用作上下文的文件路徑數量。", + "number-code-snippets": "用作上下文的代碼片段數量。", + "open-diff": "打開差異比較視圖", + "open-template-editor": "打開模板編輯器", + "overlap-size": "重疊大小", + "owner-repo-name": "此標籤將幫助您審查您的存儲庫中的拉取請求,輸入下面的所有者和存儲库名稱以開始。目前僅支持GitHub,請在設置標籤中設置您的GitHub令牌以開始。", + "path": "路徑", + "placeholder": "Twinny今天能怎麼幫助您?", + "port-placeholder": "輸入端口号,例如 '11434'", + "port": "端口", + "protocol": "協議", + "provider-connection": "接口提供者連接", + "provider-name": "接口提供者名稱", + "provider-placeholder": "輸入接口提供者名稱", + "provider-type": "接口提供者類型", + "provider": "接口提供者", + "providers": "接口提供者們", + "pull-requests": "拉取請求", + "regenerate-message": "重新生成消息", + "relevant-code-snippets": "相關代碼片段", + "relevant-file-paths": "相關文件路徑", + "repository-level": "存儲庫級別", + "rerank-probability-threshold": "重新排名概率閾值", + "rerank-threshold-description": "閾值越低,結果越有可能被包含。", + "rerank-threshold": "重新排名閾值", + "reset-providers": "重置接口提供者", + "reset-to-default": "重置為默認", + "review-pull-requests": "審查拉取請求", + "save-edit": "保存編輯", + "save": "保存", + "scroll-down": "滾動到底部", + "share-gpu-resources": "您也可以通過使用您的活躍Twinny接口提供者配置作為接口提供者連接到Symmetry來共享您的GPU資源。所有連接都是點對點的,端到端加密且安全的。", + "status": "狀態", + "stop-generation": "停止生成", + "symmetry-description": "Symmetry是一個點對點AI推理網絡,允許用戶之間進行安全、直接的連接。當您作為消費者連接時,Symmetry會根據您的模型選擇為您匹配接口提供者。", + "symmetry-inference-network": "Symmetry推理網絡", + "template-settings-description": "選擇您想要在聊天界面中使用的模板。", + "template-settings": "模板設置", + "thinking": "思考中...", + "toggle-auto-scroll": "開啟/關閉自動滾動", + "toggle-embedding-options": "開啟/關閉嵌入選項", + "toggle-provider-selection": "開啟/關閉接口提供者選擇", + "type": "類型" +} + \ No newline at end of file diff --git a/src/webview/chat.tsx b/src/webview/chat.tsx index 30665b38..06a860fe 100644 --- a/src/webview/chat.tsx +++ b/src/webview/chat.tsx @@ -305,12 +305,13 @@ export const Chat = (props: ChatProps): JSX.Element => { meta: mentions, } - global.vscode.postMessage(clientMessage) - saveLastConversation({ ...conversation, messages: updatedMessages, }) + + global.vscode.postMessage(clientMessage) + return updatedMessages }) diff --git a/src/webview/hooks.ts b/src/webview/hooks.ts index 5b62c6b9..01080548 100644 --- a/src/webview/hooks.ts +++ b/src/webview/hooks.ts @@ -10,6 +10,7 @@ import { import { MentionNodeAttrs } from "@tiptap/extension-mention" import { ReactRenderer } from "@tiptap/react" import { SuggestionKeyDownProps, SuggestionProps } from "@tiptap/suggestion" +import i18next from "i18next" import tippy, { Instance as TippyInstance } from "tippy.js" import { @@ -782,4 +783,24 @@ export const useSymmetryConnection = () => { } } +export const useLocale = () => { + const [locale, setLocale] = useState("en") + const [ renderKey, setRenderKey ] = useState(0) + useEffect(() => { + const messageHandler = (event: MessageEvent) => { + if (event.data.type === EVENT_NAME.twinnySetLocale) { + i18next.changeLanguage(event.data.data) + setLocale(event.data.data) + setRenderKey((prev: number) => prev + 1) + } + } + + global.vscode.postMessage({ type: EVENT_NAME.twinntGetLocale }) + + window.addEventListener("message", messageHandler) + return () => window.removeEventListener("message", messageHandler) + }, [i18next]) + return { locale, renderKey } +} + export default useAutosizeTextArea diff --git a/src/webview/i18n.ts b/src/webview/i18n.ts index 1784540b..cd29d045 100644 --- a/src/webview/i18n.ts +++ b/src/webview/i18n.ts @@ -1,31 +1,28 @@ import { initReactI18next } from "react-i18next" import i18n from "i18next" -import Backend from "i18next-http-backend" import en from "./assets/locales/en.json" - +import zhCN from "./assets/locales/zh-CN.json" +import zhHK from "./assets/locales/zh-HK.json" i18n - .use(Backend) .use(initReactI18next) .init({ fallbackLng: "en", resources: { en: { translation: en }, - }, - backend: { - loadPath: (lng: string) => `"/assets/locales"/${lng}.json`, + "zh-CN": { translation: zhCN }, + "zh-HK": { translation: zhHK } }, detection: { order: ["localStorage"], - availableLanguages: ["en"], + availableLanguages: [ + "en", + "zh-CN", + "zh-HK", + ] }, debug: true, - react: { - useSuspense: true, - transKeepBasicHtmlNodesFor: ["br", "strong", "i", "p", "u"], - transWrapTextNodes: "span", - }, }) export default i18n diff --git a/src/webview/main.tsx b/src/webview/main.tsx index 0c177157..f209510a 100644 --- a/src/webview/main.tsx +++ b/src/webview/main.tsx @@ -7,6 +7,7 @@ import { ServerMessage } from "../common/types" import { Chat } from "./chat" import { ConversationHistory } from "./conversation-history" +import { useLocale } from "./hooks" import { Providers } from "./providers" import { Review } from "./review" import { Settings } from "./settings" @@ -16,7 +17,7 @@ const tabs: Record = { [WEBUI_TABS.settings]: , [WEBUI_TABS.providers]: , [WEBUI_TABS.symmetry]: , - [WEBUI_TABS.review]: , + [WEBUI_TABS.review]: } interface MainProps { @@ -25,9 +26,9 @@ interface MainProps { export const Main = ({ fullScreen }: MainProps) => { const [tab, setTab] = useState(WEBUI_TABS.chat) - + const { locale, renderKey } = useLocale() const tabsWithProps = { - [WEBUI_TABS.chat]: , + [WEBUI_TABS.chat]: } const handler = (event: MessageEvent) => { @@ -53,5 +54,9 @@ export const Main = ({ fullScreen }: MainProps) => { const element: JSX.Element = allTabs[tab] - return element || null + return ( +
+ {element} +
+ ) } diff --git a/src/webview/providers.tsx b/src/webview/providers.tsx index 97d3c9f9..d4d39da7 100644 --- a/src/webview/providers.tsx +++ b/src/webview/providers.tsx @@ -245,7 +245,7 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) { return (
- +