From 50830c23939cd726765b59f0d4150230299ab070 Mon Sep 17 00:00:00 2001 From: chenchen Date: Wed, 13 May 2026 14:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=200.1.1=20=E2=80=94=20UX=20fixes?= =?UTF-8?q?=20+=20multiSelect=20+=20clean=20About=20+=20version=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Roll-up of tonight's desktop fixes shipped as 0.1.1: - PermissionDialog: cap diff/command preview at max-h-360 so the allow/deny buttons stay above the fold on huge writes (user reported scrolling 900 lines to find the buttons). - SessionTaskBar + cliTaskStore: hover row shows ✓ manual-close button for tasks the agent forgot to mark completed. - AskUserQuestion: honor schema's `multiSelect: true` — toggle membership across options, switch round → square indicator, show "可多选" hint, join answers with comma. - HeicodeLoginPage: removed dead `'official'` filter that blocked the typescript build (legacy provider id no longer in the union). - Settings About: removed the third-party social-media block and unused openUrl helper. - i18n: replaced misleading "GitHub Releases" wording with "Heicode 官方更新源" / "Heicode update source" — actual channel is Azure Blob (msi/updater/latest.json) per tauri.conf.json. - release-desktop.mjs: az invocations now run with shell:true so the Windows `az.cmd` resolves; also uploads the manifest to blob (primary endpoint) as the script finishes. - tauri.conf.json + package.json + Cargo.toml: version bumped to 0.1.1. - website/public/updater/latest.json: now reflects 0.1.1 + new signed NSIS URL (mirrored to blob by the release script). Co-Authored-By: Claude Opus 4.7 (1M context) --- cc-haha/desktop/package.json | 2 +- cc-haha/desktop/scripts/release-desktop.mjs | 19 +++-- cc-haha/desktop/src-tauri/Cargo.lock | 2 +- cc-haha/desktop/src-tauri/Cargo.toml | 2 +- cc-haha/desktop/src-tauri/tauri.conf.json | 3 +- .../src/components/chat/AskUserQuestion.tsx | 72 ++++++++++++++----- .../src/components/chat/PermissionDialog.tsx | 7 +- .../src/components/chat/SessionTaskBar.tsx | 17 ++++- .../src/components/login/HeicodeLoginPage.tsx | 17 ++--- cc-haha/desktop/src/i18n/locales/en.ts | 5 +- cc-haha/desktop/src/i18n/locales/zh.ts | 5 +- cc-haha/desktop/src/pages/Settings.tsx | 31 -------- cc-haha/desktop/src/stores/cliTaskStore.ts | 18 +++++ website/public/updater/latest.json | 10 +-- 14 files changed, 132 insertions(+), 78 deletions(-) diff --git a/cc-haha/desktop/package.json b/cc-haha/desktop/package.json index 367fe2d..cb38e0c 100644 --- a/cc-haha/desktop/package.json +++ b/cc-haha/desktop/package.json @@ -1,7 +1,7 @@ { "name": "heicode-desktop", "private": true, - "version": "0.1.0", + "version": "0.1.1", "type": "module", "scripts": { "dev": "vite", diff --git a/cc-haha/desktop/scripts/release-desktop.mjs b/cc-haha/desktop/scripts/release-desktop.mjs index 70cb9be..13b3c7d 100644 --- a/cc-haha/desktop/scripts/release-desktop.mjs +++ b/cc-haha/desktop/scripts/release-desktop.mjs @@ -86,7 +86,9 @@ function uploadBlob(localPath, remoteName) { '--overwrite', '--no-progress', ], - { stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8' }, + // On Windows `az` is `az.cmd`. shell:true so node finds it on PATH + // without us having to detect the platform. + { stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', shell: true }, ) if (result.status !== 0) { console.error(result.stderr) @@ -136,8 +138,13 @@ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) console.log(`\nWrote ${manifestPath}`) console.log(`Version : ${version}`) console.log(`Platforms: ${Object.keys(platforms).join(', ')}`) -console.log('\nNext: commit + push website to redeploy Azure SWA.') -console.log(` cd ${join(REPO_ROOT, 'website')}`) -console.log(` git add public/updater/latest.json`) -console.log(` git commit -m "release: desktop ${version}"`) -console.log(' git push') + +// Also upload manifest to blob so the blob-hosted endpoint resolves +// without depending on the website SWA deploy chain. tauri.conf.json +// lists the blob endpoint first so this is the primary update channel. +console.log('\nUploading manifest to blob (primary endpoint) ...') +uploadBlob(manifestPath, 'updater/latest.json') +console.log(`Manifest now live at ${PUBLIC_HOST}/updater/latest.json`) + +console.log('\nOptional: commit + push website to redeploy SWA fallback.') +console.log(` cd ${join(REPO_ROOT, 'website')} && git add public/updater/latest.json && git commit && git push`) diff --git a/cc-haha/desktop/src-tauri/Cargo.lock b/cc-haha/desktop/src-tauri/Cargo.lock index 5caf28c..d91bbc9 100644 --- a/cc-haha/desktop/src-tauri/Cargo.lock +++ b/cc-haha/desktop/src-tauri/Cargo.lock @@ -1525,7 +1525,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "heicode-desktop" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "portable-pty", diff --git a/cc-haha/desktop/src-tauri/Cargo.toml b/cc-haha/desktop/src-tauri/Cargo.toml index 699c523..4993878 100644 --- a/cc-haha/desktop/src-tauri/Cargo.toml +++ b/cc-haha/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "heicode-desktop" -version = "0.1.0" +version = "0.1.1" edition = "2021" [lib] diff --git a/cc-haha/desktop/src-tauri/tauri.conf.json b/cc-haha/desktop/src-tauri/tauri.conf.json index 4174b60..f046c4b 100644 --- a/cc-haha/desktop/src-tauri/tauri.conf.json +++ b/cc-haha/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json", "productName": "HeiCode", - "version": "0.1.0", + "version": "0.1.1", "identifier": "com.heicode.desktop", "build": { "frontendDist": "../dist", @@ -33,6 +33,7 @@ "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI3Q0FBOUQ2MTgwRkM4NjQKUldSa3lBOFkxcW5LdDhLUFZnNUlEdmllY0ZudjFhb2VXTUpSWmpkUmM1cjdoNDFwRW1MZTN5Yi8K", "endpoints": [ + "https://heicodeblob.blob.core.windows.net/msi/updater/latest.json", "https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json" ], "windows": { diff --git a/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx b/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx index 9369454..bece547 100644 --- a/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx +++ b/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx @@ -13,12 +13,24 @@ type Question = { question: string header?: string options?: QuestionOption[] + multiSelect?: boolean } type AskUserInput = { questions?: Question[] question?: string options?: QuestionOption[] + multiSelect?: boolean +} + +// One question's selection state. For multi-select questions this is the +// array of chosen labels; for single-select it's a single string. Indexed +// by the question's tab index in the parsed `questions` array. +type Selection = string | string[] + +function selectionToArray(sel: Selection | undefined): string[] { + if (!sel) return [] + return Array.isArray(sel) ? sel : [sel] } type Props = { @@ -41,7 +53,11 @@ function parseInput(input: unknown): Question[] { // Shape 2: { question: "...", options: [...] } if (typeof obj.question === 'string') { - return [{ question: obj.question, options: obj.options }] + return [{ + question: obj.question, + options: obj.options, + multiSelect: obj.multiSelect, + }] } return [] @@ -55,7 +71,7 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { const questions = parseInput(input) const inputObject = (input && typeof input === 'object') ? input as Record : {} const [activeTab, setActiveTab] = useState(0) - const [selections, setSelections] = useState>({}) + const [selections, setSelections] = useState>({}) const [freeText, setFreeText] = useState('') const [hasSubmitted, setHasSubmitted] = useState(false) const composingRef = useRef(false) @@ -78,18 +94,32 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { .filter((answer): answer is string => typeof answer === 'string' && answer.trim().length > 0) .join(', ') } - return freeText.trim() || Object.values(selections).join(', ') + return freeText.trim() || Object.values(selections).map((s) => selectionToArray(s).join(', ')).join(', ') }, [freeText, questions, resultAnswers, selections]) const submitted = Object.keys(resultAnswers).length > 0 || hasSubmitted const handleSelect = (qIndex: number, label: string) => { if (submitted) return + const isMulti = !!questions[qIndex]?.multiSelect setSelections((prev) => { - // Toggle: deselect if already selected + if (isMulti) { + const current = selectionToArray(prev[qIndex]) + const next = current.includes(label) + ? current.filter((x) => x !== label) + : [...current, label] + const out = { ...prev } + if (next.length === 0) { + delete out[qIndex] + } else { + out[qIndex] = next + } + return out + } + // Single-select: toggle off if already chosen, otherwise replace. if (prev[qIndex] === label) { - const next = { ...prev } - delete next[qIndex] - return next + const out = { ...prev } + delete out[qIndex] + return out } return { ...prev, [qIndex]: label } }) @@ -101,8 +131,8 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { const parts: string[] = [] for (let i = 0; i < questions.length; i++) { - const selected = selections[i] - if (selected) parts.push(selected) + const selected = selectionToArray(selections[i]) + if (selected.length > 0) parts.push(selected.join(', ')) } const response = freeText.trim() || parts.join('; ') || '' if (!response) return @@ -112,8 +142,9 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { const answers = questions.reduce>((acc, question, index) => { if (freeText.trim()) { acc[question.question] = freeText.trim() - } else if (selections[index]) { - acc[question.question] = selections[index]! + } else { + const sel = selectionToArray(selections[index]) + if (sel.length > 0) acc[question.question] = sel.join(', ') } return acc }, {}) @@ -128,7 +159,7 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { } // All questions must be answered (via selection or free text) to enable submit - const allAnswered = freeText.trim().length > 0 || questions.every((_, i) => selections[i] !== undefined) + const allAnswered = freeText.trim().length > 0 || questions.every((_, i) => selectionToArray(selections[i]).length > 0) const safeActiveTab = Math.min(activeTab, questions.length - 1) const activeQuestion = questions[safeActiveTab] @@ -168,7 +199,7 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) {
{questions.map((q, i) => { const isActive = safeActiveTab === i - const isAnswered = selections[i] !== undefined + const isAnswered = selectionToArray(selections[i]).length > 0 const tabLabel = q.header || `Q${i + 1}` return ( + )}
) } diff --git a/cc-haha/desktop/src/components/login/HeicodeLoginPage.tsx b/cc-haha/desktop/src/components/login/HeicodeLoginPage.tsx index ac2c2b7..0a7e33f 100644 --- a/cc-haha/desktop/src/components/login/HeicodeLoginPage.tsx +++ b/cc-haha/desktop/src/components/login/HeicodeLoginPage.tsx @@ -135,22 +135,19 @@ export function HeicodeLoginPage() { {/* docs/product-package/08 §"登录" + §"客户端不应该出现的内容": Login screen only shows the Heicode provider. The legacy - `official` (Claude Official) preset is filtered out so the - login screen carries Heicode brand alone, no third-party - route entry as docs §8 forbids. */} - {hasFetched && providers.filter((p) => p.id !== 'official').length === 0 ? ( + `official` (Claude Official) preset was removed from the + provider type entirely; no filter needed any more. */} + {hasFetched && providers.length === 0 ? (
{t('login.errors.noProviders')}
) : null} - {hasFetched && providers.filter((p) => p.id !== 'official').length > 0 ? ( + {hasFetched && providers.length > 0 ? (
- {providers - .filter((provider) => provider.id !== 'official') - .map((provider) => ( - - ))} + {providers.map((provider) => ( + + ))}
) : null} diff --git a/cc-haha/desktop/src/i18n/locales/en.ts b/cc-haha/desktop/src/i18n/locales/en.ts index 778983e..e13af55 100644 --- a/cc-haha/desktop/src/i18n/locales/en.ts +++ b/cc-haha/desktop/src/i18n/locales/en.ts @@ -432,7 +432,7 @@ export const en = { 'settings.about.author': 'Author', 'settings.about.socialMedia': 'Social Media', 'settings.about.updates': 'App Updates', - 'settings.about.updatesDesc': 'Check GitHub Releases, download the installer, and relaunch after install.', + 'settings.about.updatesDesc': 'Check the Heicode update source, download the installer, and relaunch after install.', // Settings > Computer Use 'settings.tab.computerUse': 'Computer Use', @@ -661,6 +661,7 @@ export const en = { 'question.typePlaceholder': 'Type your answer...', 'question.submit': 'Submit', 'question.answeredPrefix': 'Answered: ', + 'question.multiSelectHint': 'Multiple choice — click to toggle', // ─── Thinking Block ────────────────────────────────────── 'thinking.label': 'Thinking', @@ -909,7 +910,7 @@ export const en = { 'update.currentVersionUnknown': 'Unknown', 'update.newVersion': 'New version v{version} available', 'update.downloading': 'Downloading...', - 'update.idle': 'Check for updates to compare your installed version with the latest GitHub Release.', + 'update.idle': 'Check for updates to compare your installed version with the latest available release.', 'update.now': 'Update now', 'update.later': 'Later', 'update.progress': 'Downloading update... {progress}%', diff --git a/cc-haha/desktop/src/i18n/locales/zh.ts b/cc-haha/desktop/src/i18n/locales/zh.ts index b9806ac..c214083 100644 --- a/cc-haha/desktop/src/i18n/locales/zh.ts +++ b/cc-haha/desktop/src/i18n/locales/zh.ts @@ -434,7 +434,7 @@ export const zh: Record = { 'settings.about.author': '作者', 'settings.about.socialMedia': '社交媒体', 'settings.about.updates': '应用更新', - 'settings.about.updatesDesc': '检查 GitHub Releases,下载安装包,并在安装后自动重启。', + 'settings.about.updatesDesc': '检查 Heicode 官方更新源,下载安装包,并在安装后自动重启。', // Settings > Computer Use 'settings.tab.computerUse': 'Computer Use', @@ -663,6 +663,7 @@ export const zh: Record = { 'question.typePlaceholder': '输入你的回答...', 'question.submit': '提交', 'question.answeredPrefix': '已回答: ', + 'question.multiSelectHint': '可多选(点击切换)', // ─── Thinking Block ────────────────────────────────────── 'thinking.label': '思考中', @@ -911,7 +912,7 @@ export const zh: Record = { 'update.currentVersionUnknown': '未知版本', 'update.newVersion': '新版本 v{version} 可用', 'update.downloading': '下载中...', - 'update.idle': '点击检查更新,对比当前安装版本和 GitHub Releases 的最新版本。', + 'update.idle': '点击检查更新,对比当前安装版本和官方更新源的最新版本。', 'update.now': '立即更新', 'update.later': '稍后', 'update.progress': '正在下载更新... {progress}%', diff --git a/cc-haha/desktop/src/pages/Settings.tsx b/cc-haha/desktop/src/pages/Settings.tsx index 8534d08..fa0ef5d 100644 --- a/cc-haha/desktop/src/pages/Settings.tsx +++ b/cc-haha/desktop/src/pages/Settings.tsx @@ -1554,12 +1554,6 @@ function PluginSettings() { // ─── About Settings ────────────────────────────────────── -const SOCIAL_LINKS = [ - { name: 'Bilibili', icon: '/icons/bilibili.svg', url: 'https://space.bilibili.com/434377496', label: '程序员阿江-Relakkes' }, - { name: 'Douyin', icon: '/icons/douyin.svg', url: 'https://www.douyin.com/user/MS4wLjABAAAATJPY7LAlaa5X-c8uNdWkvz0jUGgpw4eeXIwu_8BhvqE', label: '程序员阿江-Relakkes' }, - { name: 'Xiaohongshu', icon: '/icons/xiaohongshu.svg', url: 'https://www.xiaohongshu.com/user/profile/5f58bd990000000001003753', label: '程序员阿江-Relakkes' }, -] as const - function AboutSettings() { const t = useTranslation() const [version, setVersion] = useState('') @@ -1596,10 +1590,6 @@ function AboutSettings() { void initialize() }, [initialize]) - const openUrl = (url: string) => { - import('@tauri-apps/plugin-shell').then((mod) => mod.open(url)).catch(() => window.open(url, '_blank')) - } - const checkedAtText = checkedAt ? new Date(checkedAt).toLocaleString(undefined, { @@ -1739,27 +1729,6 @@ function AboutSettings() { - {/* Divider */} -
- - {/* Social Media */} -
-

{t('settings.about.socialMedia')}

-
- {SOCIAL_LINKS.map((link) => ( - - ))} -
-
-
) } diff --git a/cc-haha/desktop/src/stores/cliTaskStore.ts b/cc-haha/desktop/src/stores/cliTaskStore.ts index 3b8db3a..95fd209 100644 --- a/cc-haha/desktop/src/stores/cliTaskStore.ts +++ b/cc-haha/desktop/src/stores/cliTaskStore.ts @@ -29,6 +29,10 @@ type CLITaskStore = { refreshTasks: () => Promise /** Update tasks from TodoWrite V1 tool input (in-memory, no disk read needed) */ setTasksFromTodos: (todos: TodoItem[]) => void + /** Manually flip a stuck task to 'completed' (local override; a later + * TodoWrite would still re-sync from the agent's truth). For when the + * model forgets to close a task and the user wants to clear the panel. */ + markTaskCompletedLocally: (taskId: string) => void /** Mark that completed tasks were already dismissed (conversation continued) */ markCompletedAndDismissed: () => void /** Clear a completed task list locally and remotely so the next cycle starts clean */ @@ -140,6 +144,20 @@ export const useCLITaskStore = create((set, get) => ({ })) }, + markTaskCompletedLocally: (taskId) => { + set((state) => { + const tasks = state.tasks.map((task) => + task.id === taskId && task.status !== 'completed' + ? { ...task, status: 'completed' as TaskStatus, activeForm: undefined } + : task, + ) + return { + tasks, + ...resolveDismissState(tasks, state.dismissedCompletionKey), + } + }) + }, + markCompletedAndDismissed: () => { const completionKey = buildCompletedTaskKey(get().tasks) if (!completionKey) return diff --git a/website/public/updater/latest.json b/website/public/updater/latest.json index 9418ebc..baf525a 100644 --- a/website/public/updater/latest.json +++ b/website/public/updater/latest.json @@ -1,11 +1,11 @@ { - "version": "0.1.0", - "notes": "0.1.0 首次发版 — wallet §4 实时数据 + StreamingIndicator 工具进度显示 + 更新通道闭环验证", - "pub_date": "2026-05-13T04:54:40.158Z", + "version": "0.1.1", + "notes": "0.1.1 — 审批弹窗按钮不再被大 diff 挤走、任务卡死可手动 ✓ 闭合、AskUserQuestion 支持可多选、文案修正", + "pub_date": "2026-05-13T06:06:08.282Z", "platforms": { "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSa3lBOFkxcW5LdDYwMjd0VHQyaThMY3F4MUI3TzdqY1ZVaFZHOEM0TWovK0ZqN25pZ1VmbWJieTBmaXg4UkM5MWpVTHM4UFNRamZsUS9jNncvQldQVDZqUGRwajA4WHdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc4NjQ2MDQ3CWZpbGU6SGVpQ29kZV8wLjEuMF94NjQtc2V0dXAuZXhlCktUNVBUYWZCZVNFZFJXU2JqRGFtejVjU2FMbDRNSFlSWlYxVVFidE1TWmxTemtJRlRDekY3dEtEdFE0N0poenZrMjVNditHTTNXS0tkK002cG1QN0FBPT0K", - "url": "https://heicodeblob.blob.core.windows.net/msi/desktop/0.1.0/HeiCode_0.1.0_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSa3lBOFkxcW5LdHhtQUxDK2h0YWc3aHhBM3N5NFF3TEgxMFV1V285dzdZRkpIT1VTdTl4b2pXekplNHBRQmpHNktmbitocjc3dEdkZmUyQUNwbUVUaHN5V1BUS2NwTGdRPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc4NjUyMzE0CWZpbGU6SGVpQ29kZV8wLjEuMV94NjQtc2V0dXAuZXhlCk9hMkF0azhCUnRSNjZIZ1VmN1BTZi9Kdmowdk93aEltcDI3bGlsRm9RcHJiMjAwQkRhTytQd0lyaVBFYXg1Vk0rQjE5MXZaSkcwM0FINThrY2d2cUNBPT0K", + "url": "https://heicodeblob.blob.core.windows.net/msi/desktop/0.1.1/HeiCode_0.1.1_x64-setup.exe" } } }