feat(desktop): 0.1.1 — UX fixes + multiSelect + clean About + version bump
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "heicode-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Generated
+1
-1
@@ -1525,7 +1525,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heicode-desktop"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"portable-pty",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "heicode-desktop"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<string, unknown> : {}
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [selections, setSelections] = useState<Record<number, string>>({})
|
||||
const [selections, setSelections] = useState<Record<number, Selection>>({})
|
||||
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<Record<string, string>>((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) {
|
||||
<div className="flex px-4 border-b border-[var(--color-outline-variant)]/20 bg-[var(--color-surface-container-low)] overflow-x-auto">
|
||||
{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 (
|
||||
<button
|
||||
@@ -202,8 +233,15 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) {
|
||||
{/* Option cards */}
|
||||
{activeQuestion.options && activeQuestion.options.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
{activeQuestion.multiSelect && (
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)] mb-1">
|
||||
{t('question.multiSelectHint')}
|
||||
</p>
|
||||
)}
|
||||
{activeQuestion.options.map((opt, optIndex) => {
|
||||
const isSelected = selections[activeTab] === opt.label
|
||||
const selectedLabels = selectionToArray(selections[safeActiveTab])
|
||||
const isSelected = selectedLabels.includes(opt.label)
|
||||
const isMulti = !!activeQuestion.multiSelect
|
||||
return (
|
||||
<button
|
||||
key={optIndex}
|
||||
@@ -216,8 +254,10 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) {
|
||||
} ${submitted ? 'cursor-default' : ''}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Check indicator */}
|
||||
<div className={`mt-0.5 flex-shrink-0 w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors ${
|
||||
{/* Check indicator — square for multi-select, circle for single. */}
|
||||
<div className={`mt-0.5 flex-shrink-0 w-4 h-4 border-2 flex items-center justify-center transition-colors ${
|
||||
isMulti ? 'rounded-[3px]' : 'rounded-full'
|
||||
} ${
|
||||
isSelected
|
||||
? 'border-[var(--color-secondary)] bg-[var(--color-secondary)]'
|
||||
: 'border-[var(--color-outline)]'
|
||||
|
||||
@@ -184,7 +184,12 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr
|
||||
<span className="truncate">{details.primary}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{preview}
|
||||
{/* Cap preview height so big diffs / long bash commands don't push
|
||||
the action buttons below the fold. User scrolls inside the
|
||||
preview to see the rest; allow/deny stay reachable. */}
|
||||
<div className="max-h-[360px] overflow-auto rounded-[var(--radius-md)]">
|
||||
{preview}
|
||||
</div>
|
||||
</div>
|
||||
) : details.primary ? (
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -114,9 +114,10 @@ export function SessionTaskBar() {
|
||||
|
||||
function TaskItem({ task }: { task: CLITask }) {
|
||||
const config = statusConfig[task.status]
|
||||
const markTaskCompletedLocally = useCLITaskStore((s) => s.markTaskCompletedLocally)
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2 py-1.5 px-1 rounded-md">
|
||||
<div className="group flex items-start gap-2 py-1.5 px-1 rounded-md">
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px] mt-px shrink-0"
|
||||
style={{ color: config.color, fontVariationSettings: "'FILL' 1" }}
|
||||
@@ -154,6 +155,20 @@ function TaskItem({ task }: { task: CLITask }) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Manual override for when the agent forgets to close an in_progress
|
||||
task. Only shown on hover for non-completed rows. Click marks it
|
||||
completed locally; a later TodoWrite from the agent still wins. */}
|
||||
{task.status !== 'completed' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => markTaskCompletedLocally(task.id)}
|
||||
title="标记完成 / Mark as done"
|
||||
className="opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity shrink-0 mt-0.5 px-1 py-0.5 rounded text-[10px] text-[var(--color-text-tertiary)] hover:text-[var(--color-success)] hover:bg-[var(--color-surface-container-high)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] align-middle">check</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="max-w-sm rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] p-4 text-sm text-[var(--color-error)]">
|
||||
{t('login.errors.noProviders')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasFetched && providers.filter((p) => p.id !== 'official').length > 0 ? (
|
||||
{hasFetched && providers.length > 0 ? (
|
||||
<div className="grid w-full max-w-sm grid-cols-1 gap-4">
|
||||
{providers
|
||||
.filter((provider) => provider.id !== 'official')
|
||||
.map((provider) => (
|
||||
<ProviderLoginCard key={provider.id} provider={provider} />
|
||||
))}
|
||||
{providers.map((provider) => (
|
||||
<ProviderLoginCard key={provider.id} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -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}%',
|
||||
|
||||
@@ -434,7 +434,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'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<TranslationKey, string> = {
|
||||
'question.typePlaceholder': '输入你的回答...',
|
||||
'question.submit': '提交',
|
||||
'question.answeredPrefix': '已回答: ',
|
||||
'question.multiSelectHint': '可多选(点击切换)',
|
||||
|
||||
// ─── Thinking Block ──────────────────────────────────────
|
||||
'thinking.label': '思考中',
|
||||
@@ -911,7 +912,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'update.currentVersionUnknown': '未知版本',
|
||||
'update.newVersion': '新版本 v{version} 可用',
|
||||
'update.downloading': '下载中...',
|
||||
'update.idle': '点击检查更新,对比当前安装版本和 GitHub Releases 的最新版本。',
|
||||
'update.idle': '点击检查更新,对比当前安装版本和官方更新源的最新版本。',
|
||||
'update.now': '立即更新',
|
||||
'update.later': '稍后',
|
||||
'update.progress': '正在下载更新... {progress}%',
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-full border-t border-[var(--color-border)]/40 my-6" />
|
||||
|
||||
{/* Social Media */}
|
||||
<div className="w-full mt-4">
|
||||
<h3 className="text-xs font-medium text-[var(--color-text-tertiary)] uppercase tracking-wider mb-3">{t('settings.about.socialMedia')}</h3>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{SOCIAL_LINKS.map((link) => (
|
||||
<button
|
||||
key={link.name}
|
||||
onClick={() => openUrl(link.url)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors cursor-pointer"
|
||||
>
|
||||
<img src={link.icon} alt={link.name} className="w-4 h-4 opacity-60" />
|
||||
<span className="text-sm text-[var(--color-text-primary)]">{link.label}</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)] ml-auto">{link.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ type CLITaskStore = {
|
||||
refreshTasks: () => Promise<void>
|
||||
/** 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<CLITaskStore>((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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user