From a8967f81de641d973f3e44cc91b506a8f1642ad3 Mon Sep 17 00:00:00 2001 From: A-znk <145305286+Yuki-zik@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:14:09 +0800 Subject: [PATCH] feat(highlights): add command to set highlight scheme for all tags Closes #2 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 + README_zh.md | 2 + agent/tasks.md | 10 ++--- agent/timeline.md | 3 +- package.json | 11 ++++++ package.nls.json | 1 + package.nls.zh-cn.json | 1 + src/extension.js | 50 +++++++++++++++++++++++++ src/highlightScheme.js | 33 +++++++++++++++++ test/extension.tests.js | 30 ++++++++++++++- test/highlightScheme.tests.js | 69 +++++++++++++++++++++++++++++++++++ 11 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/highlightScheme.js create mode 100644 test/highlightScheme.tests.js diff --git a/README.md b/README.md index feb5e99..b602666 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ TaskVision uses four independent styling channels: > `scheme` controls presets only — it no longer decides scope. +**Apply a scheme to every tag at once:** run **TaskVision: Set Highlight Scheme for All Tags** from the Command Palette or the tree view's `⋯` overflow menu, then pick `neon`, `glass`, `neon+glass`, or `none`. TaskVision writes the choice into every existing `customHighlight` entry **and** `defaultHighlight`, so you no longer have to edit each tag by hand. + --- ## Quick Start diff --git a/README_zh.md b/README_zh.md index 77ef73b..1634da8 100644 --- a/README_zh.md +++ b/README_zh.md @@ -233,6 +233,8 @@ TaskVision 使用四个独立样式通道: > `scheme` 只控制预设启用,不再决定作用范围。 +**一次性为所有标签设置方案:** 在命令面板或树视图标题栏的 `⋯` 溢出菜单中运行 **TaskVision: 为所有标签设置高亮方案**,然后选择 `neon`、`glass`、`neon+glass` 或 `none`。TaskVision 会把所选方案写入每一个已存在的 `customHighlight` 条目**以及** `defaultHighlight`,无需再逐个标签手动修改。 + --- ## 快速开始 diff --git a/agent/tasks.md b/agent/tasks.md index a208c23..95186ad 100644 --- a/agent/tasks.md +++ b/agent/tasks.md @@ -9,11 +9,11 @@ ## Session Summary -- Active focus: Reanalyzed the AI context feature's data flow, product logic, and agent handoff contract after sidecar write hardening. -- Verification: read-only source/docs/tests review plus `git --no-pager ls-files .taskvision .gitignore`; no test suite was run because no runtime code changed. +- Active focus: Implemented issue #2 — added a command to uniformly set the highlight scheme for all tags, resolving that shipped per-tag `customHighlight` entries hid `defaultHighlight`. +- Verification: `npx qunit test/highlightScheme.tests.js` (5 passing), `npm test` (126 passing), `npm run webpack`, `git --no-pager diff --check` (clean). ## Active Session Task -| Priority | Task | Status | Owner | Due | -| -------- | ----------------------------- | --------- | ----- | ---------- | -| P1 | Reanalyze AI context logic | Completed | AI | 2026-04-29 | +| Priority | Task | Status | Owner | Due | +| -------- | ------------------------------------------------ | --------- | ----- | ---------- | +| P1 | Add "set highlight scheme for all tags" command | Completed | AI | 2026-07-08 | diff --git a/agent/timeline.md b/agent/timeline.md index 55e172f..9289ee1 100644 --- a/agent/timeline.md +++ b/agent/timeline.md @@ -2,7 +2,8 @@ | 日期时间 | 任务/变更 | 修改文件 | 实现逻辑 | 修改动机 | 结果/备注 | | ---------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-04-29 20:45 | 重新分析 AI 上下文功能逻辑与协作思路 | agent/tasks.md, agent/timeline.md | 只读梳理 `aiContext` 导出器、扩展命令链路、task/context/session sidecar store、稳定 ID 生成、README 协议说明、配置 schema 与测试覆盖,并确认 `.taskvision` 生成文件当前被 Git 跟踪 | 用户要求重新检查 AI 上下文功能部分的逻辑与思路是否合理,需要在继续实现前判断产品定位、数据源边界、会话语义和协作契约是否自洽 | 已完成分析;结论为“外部 agent 交接包”的核心方向合理,但 implementation session 未进入导出、sidecar/source 权责边界、跟踪生成文件、状态报告语义和 Extension Host 集成测试仍需收敛;验证执行只读源码/文档/测试核查与 `git --no-pager ls-files .taskvision .gitignore`,未运行测试套件 | +| 2026-07-08 11:00 | 新增“为所有标签统一设置高亮方案”命令(issue #2) | src/highlightScheme.js, src/extension.js, package.json, package.nls.json, package.nls.zh-cn.json, test/highlightScheme.tests.js, test/extension.tests.js, README.md, README_zh.md, agent/tasks.md, agent/timeline.md | 新增纯函数模块 `highlightScheme.applySchemeToAllTags`:深拷贝后把所选 scheme 写入每个已存在的 customHighlight 条目及 defaultHighlight(`none` 则删除 scheme 键,保留其他属性、不改动入参);在 extension.js 注册 `taskvision.setHighlightSchemeForAllTags`,QuickPick 选择 neon/glass/neon+glass/none 后经 `resolveConfigTarget()` 依次写入 customHighlight 与 defaultHighlight,复用既有配置变更监听自动刷新高亮;package.json 贡献命令并加入树视图标题溢出菜单,补齐中英文 NLS,更新 README | issue #2:用户希望一次性统一设置所有 tag 的高亮方案,而内置 per-tag customHighlight 会覆盖 defaultHighlight,导致 defaultHighlight 看似无效 | 已完成;验证执行 `npx qunit test/highlightScheme.tests.js`(5 passing)、`npm test`(126 passing)、`npm run webpack`、`git --no-pager diff --check`(无输出)| +| 2026-04-29 20:45 | 重新分析 AI 上下文功能逻辑与协作思路 | agent/tasks.md, agent/timeline.md | 只读梳理 `aiContext` 导出器、扩展命令链路、task/context/session sidecar store、稳定 ID 生成、README 协议说明、配置 schema 与测试覆盖,并确认 `.taskvision` 生成文件当前被 Git 跟踪 | 用户要求重新检查 AI 上下文功能部分的逻辑与思路是否合理,需要在继续实现前判断产品定位、数据源边界、会话语义和协作契约是否自洽 | 已完成分析;结论为“外部 agent 交接包”的核心方向合理,但 implementation session 未进入导出、sidecar/source 权责边界、跟踪生成文件、状态报告语义和 Extension Host 集成测试仍需收敛;验证执行只读源码/文档/测试核查与 `git --no-pager ls-files .taskvision .gitignore`,未运行测试套件 | | 2026-04-29 20:18 | 加固 AI sidecar JSON 写入与损坏恢复 | src/jsonStore.js, src/taskMetaStore.js, src/contextStore.js, src/changeSessionStore.js, test/dataModel.tests.js, test/extension.tests.js, agent/tasks.md, agent/timeline.md | 新增共享 `jsonStore`,用同目录临时文件加 `rename` 替换写入 JSON;读取 malformed JSON 时先重命名为 `.invalid` 备份再恢复为空 store;任务与上下文 store 在写入失败后失效内存 cache,避免未落盘变更被下一次保存带回;补充 malformed 备份、原子替换失败和 cache 丢弃回归测试 | 用户要求开始实现 AI 可靠性改进,需要优先降低 `.taskvision` sidecar 被半写、损坏或静默覆盖时的协作风险 | 已完成;验证执行 `npx qunit test/dataModel.tests.js test/aiContext.tests.js test/extension.tests.js`、`npm test`(119 passing)、`npm run webpack`、`git diff --check -- agent/tasks.md agent/timeline.md src/jsonStore.js src/taskMetaStore.js src/contextStore.js src/changeSessionStore.js test/dataModel.tests.js test/extension.tests.js` | | 2026-04-29 19:00 | 修复深度 review 中确认的问题 | buildCodiconNames.js, package.json, package.nls.json, package.nls.zh-cn.json, src/config.js, src/extension.js, src/icons.js, src/taskMetaStore.js, test/aiContext.tests.js, test/extension.tests.js, test/schemes.tests.js, agent/tasks.md, agent/timeline.md | 为 AI 输出目录增加 workspace 内约束与 schema 校验,修正 `file excludes` 枚举判断,移除 regex 配置变更早退并让 opacity 变更清高亮缓存,合并字体样式配置写入,修正 TaskVision 内置图标校验条件,补齐命令 NLS key,并让 codicon 生成失败时中止构建而不是写入极小 fallback;同时补充对应回归测试 | 用户要求开始修复全系统深度 review 中已确认的问题,需要用测试先行方式把行为锁住并避免误改未验证项 | 已完成;验证执行 `npx qunit test/aiContext.tests.js test/extension.tests.js test/schemes.tests.js`、`npm test`(112 passing)、`npm run webpack`、`git diff --check -- agent/tasks.md agent/timeline.md buildCodiconNames.js package.json package.nls.json package.nls.zh-cn.json src/config.js src/extension.js src/icons.js src/taskMetaStore.js test/aiContext.tests.js test/extension.tests.js test/schemes.tests.js` | | 2026-04-29 18:40 | 对整套系统进行深度 review | agent/tasks.md, agent/timeline.md | 用并行只读子系统审查覆盖入口/树视图、扫描解析、高亮样式、AI sidecar、配置测试与发布链路,并在主线程逐项复核候选问题,剔除与源码不符的误报后整理风险报告 | 用户要求对当前系统做全量深度 review,需要优先识别真实缺陷、行为回归风险和测试缺口 | 已完成;验证执行 `npm test`(104 passing)、`npm run webpack`、`git diff --check -- agent/tasks.md agent/timeline.md` | diff --git a/package.json b/package.json index f74812c..f27aa41 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,11 @@ "when": "view =~ /taskvision/", "group": "navigation@1.3" }, + { + "command": "taskvision.setHighlightSchemeForAllTags", + "when": "view =~ /taskvision/", + "group": "1-highlights@1" + }, { "command": "taskvision.reveal", "when": "view =~ /taskvision/ && taskvision-tags-only == false && taskvision-show-reveal-button == true", @@ -674,6 +679,12 @@ "command": "taskvision.customizeAppearance", "title": "Customize Tag Appearance", "category": "%taskvision.command.category%" + }, + { + "command": "taskvision.setHighlightSchemeForAllTags", + "title": "%taskvision.command.setHighlightSchemeForAllTags.title%", + "category": "%taskvision.command.category%", + "icon": "$(symbol-color)" } ], "configuration": [ diff --git a/package.nls.json b/package.nls.json index 5592a7d..db40f47 100644 --- a/package.nls.json +++ b/package.nls.json @@ -48,6 +48,7 @@ "taskvision.command.setTaskStatus.title": "Set Task Status", "taskvision.command.setTaskPriority.title": "Set Task Priority", "taskvision.command.editTaskNote.title": "Edit Task Note", + "taskvision.command.setHighlightSchemeForAllTags.title": "Set Highlight Scheme for All Tags", "taskvision.configuration.title": "TaskVision", "taskvision.configuration.filtering": "Filtering", "taskvision.configuration.filtering.excludedWorkspaces.markdownDescription": "An array of workspace names to exclude as roots in the tree (wildcards can be used).", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index cdbdaf6..15c6d56 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -48,6 +48,7 @@ "taskvision.command.setTaskStatus.title": "设置任务状态", "taskvision.command.setTaskPriority.title": "设置任务优先级", "taskvision.command.editTaskNote.title": "编辑任务备注", + "taskvision.command.setHighlightSchemeForAllTags.title": "为所有标签设置高亮方案", "taskvision.configuration.title": "待办事项树", "taskvision.configuration.filtering": "滤波", "taskvision.configuration.filtering.excludedWorkspaces.markdownDescription": "配置一个工作区名称的列表,列表中的工作区不允许作为树状图的根(允许使用通配符)。", diff --git a/src/extension.js b/src/extension.js index e993fdd..076d027 100644 --- a/src/extension.js +++ b/src/extension.js @@ -23,6 +23,7 @@ var contextStore = require('./contextStore.js'); var changeSessionStore = require('./changeSessionStore.js'); var annotationParser = require('./annotationParser.js'); var aiContext = require('./aiContext.js'); +var highlightScheme = require('./highlightScheme.js'); var searchList = []; var currentFilter; @@ -2350,6 +2351,55 @@ function activate(context) { } })); + context.subscriptions.push(vscode.commands.registerCommand('taskvision.setHighlightSchemeForAllTags', function () { + var currentConfig = vscode.workspace.getConfiguration('taskvision.highlights'); + var customHighlight = currentConfig.get('customHighlight', {}); + var defaultHighlight = currentConfig.get('defaultHighlight', {}); + var configTarget = resolveConfigTarget(); + + var items = [ + { + label: '$(zap) Neon', + detail: 'Enable glow preset for every tag', + value: 'neon' + }, + { + label: '$(browser) Glass', + detail: 'Enable glass preset for every tag', + value: 'glass' + }, + { + label: '$(sparkle) Neon + Glass', + detail: 'Enable both glow and glass presets for every tag', + value: 'neon+glass' + }, + { + label: '$(circle-slash) None', + detail: 'Clear scheme preset for every tag', + value: 'none' + } + ]; + + vscode.window.showQuickPick(items, { + placeHolder: 'Choose a highlight scheme to apply to ALL tags', + matchOnDetail: true + }).then(function (selection) { + if (!selection) { + return; + } + + var updated = highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, selection.value); + + return Promise.resolve(currentConfig.update('customHighlight', updated.customHighlight, configTarget)) + .then(function () { + return currentConfig.update('defaultHighlight', updated.defaultHighlight, configTarget); + }) + .then(function () { + vscode.window.showInformationMessage('Applied "' + selection.value + '" highlight scheme to all tags'); + }); + }); + })); + context.subscriptions.push(vscode.commands.registerCommand('taskvision.customizeAppearance', function (node) { if (node && node.tag) { var currentConfig = vscode.workspace.getConfiguration('taskvision.highlights'); diff --git a/src/highlightScheme.js b/src/highlightScheme.js new file mode 100644 index 0000000..cc3a4c4 --- /dev/null +++ b/src/highlightScheme.js @@ -0,0 +1,33 @@ +function clone(value) { + return value ? JSON.parse(JSON.stringify(value)) : {}; +} + +function shouldClear(scheme) { + return scheme === undefined || scheme === null || scheme === '' || scheme === 'none'; +} + +function applyScheme(target, scheme) { + if (!target || typeof target !== 'object') { + return; + } + if (shouldClear(scheme)) { + delete target.scheme; + } else { + target.scheme = scheme; + } +} + +function applySchemeToAllTags(customHighlight, defaultHighlight, scheme) { + var updatedCustom = clone(customHighlight); + var updatedDefault = clone(defaultHighlight); + + Object.keys(updatedCustom).forEach(function (tag) { + applyScheme(updatedCustom[tag], scheme); + }); + + applyScheme(updatedDefault, scheme); + + return { customHighlight: updatedCustom, defaultHighlight: updatedDefault }; +} + +module.exports.applySchemeToAllTags = applySchemeToAllTags; diff --git a/test/extension.tests.js b/test/extension.tests.js index 2f4388b..6c87d83 100644 --- a/test/extension.tests.js +++ b/test/extension.tests.js @@ -47,6 +47,33 @@ QUnit.test('extension updates font appearance in a single customHighlight write' assert.ok(source.indexOf('updateConfigValues({ fontWeight: font.fontWeight, fontStyle: font.fontStyle });') !== -1); }); +QUnit.test('extension registers a command to set the highlight scheme for all tags', function (assert) { + var source = fs.readFileSync('src/extension.js', 'utf8'); + + assert.ok(source.indexOf("var highlightScheme = require('./highlightScheme.js');") !== -1, 'requires the highlightScheme module'); + assert.ok(source.indexOf("vscode.commands.registerCommand('taskvision.setHighlightSchemeForAllTags'") !== -1, 'registers the command'); + assert.ok(source.indexOf('highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, selection.value)') !== -1, 'delegates to the pure helper'); + assert.ok(source.indexOf('var configTarget = resolveConfigTarget();') !== -1, 'uses resolveConfigTarget for the write target'); + assert.ok(source.indexOf("currentConfig.update('customHighlight', updated.customHighlight, configTarget)") !== -1, 'writes customHighlight with the resolved target'); + assert.ok(source.indexOf("currentConfig.update('defaultHighlight', updated.defaultHighlight, configTarget)") !== -1, 'writes defaultHighlight with the resolved target'); +}); + +QUnit.test('package contributes the set-scheme-for-all-tags command in the view title overflow menu', function (assert) { + var packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + var command = packageJson.contributes.commands.find(function (candidate) { + return candidate.command === 'taskvision.setHighlightSchemeForAllTags'; + }); + assert.ok(command, 'command is contributed'); + assert.strictEqual(command.title, '%taskvision.command.setHighlightSchemeForAllTags.title%', 'command title uses an NLS key'); + + var titleMenu = packageJson.contributes.menus['view/title'].find(function (item) { + return item.command === 'taskvision.setHighlightSchemeForAllTags'; + }); + assert.ok(titleMenu, 'command is added to the view/title menu'); + assert.ok(titleMenu.group.indexOf('navigation') === -1, 'command is placed in the overflow menu, not the navigation bar'); +}); + QUnit.test('extension source accepts built-in taskvision icons during validation', function (assert) { var source = fs.readFileSync('src/icons.js', 'utf8'); @@ -69,7 +96,8 @@ QUnit.test('package localizes contributed command titles', function (assert) { 'taskvision.addMissingTaskStatuses', 'taskvision.setTaskStatus', 'taskvision.setTaskPriority', - 'taskvision.editTaskNote' + 'taskvision.editTaskNote', + 'taskvision.setHighlightSchemeForAllTags' ]; commandIds.forEach(function (commandId) { diff --git a/test/highlightScheme.tests.js b/test/highlightScheme.tests.js new file mode 100644 index 0000000..047eedc --- /dev/null +++ b/test/highlightScheme.tests.js @@ -0,0 +1,69 @@ +var highlightScheme = require('../src/highlightScheme.js'); + +QUnit.test('applySchemeToAllTags sets scheme on every existing customHighlight entry and defaultHighlight', function (assert) { + var customHighlight = { + TODO: { scheme: 'glass', background: '#123456', colorType: 'text' }, + FIXME: { scheme: 'neon+glass', background: '#9C27B0', icon: 'flame' }, + NOTE: { background: '#00E5FF' } + }; + var defaultHighlight = { background: '#5C6BC0', scheme: 'neon+glass', icon: 'tag' }; + + var result = highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, 'neon'); + + assert.strictEqual(result.customHighlight.TODO.scheme, 'neon'); + assert.strictEqual(result.customHighlight.FIXME.scheme, 'neon'); + assert.strictEqual(result.customHighlight.NOTE.scheme, 'neon', 'entry without prior scheme still receives one'); + assert.strictEqual(result.defaultHighlight.scheme, 'neon'); +}); + +QUnit.test('applySchemeToAllTags preserves non-scheme properties', function (assert) { + var customHighlight = { + TODO: { scheme: 'glass', background: '#123456', colorType: 'text', icon: 'check' } + }; + var defaultHighlight = { background: '#5C6BC0', icon: 'tag', rulerLane: 'center' }; + + var result = highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, 'neon+glass'); + + assert.strictEqual(result.customHighlight.TODO.background, '#123456'); + assert.strictEqual(result.customHighlight.TODO.colorType, 'text'); + assert.strictEqual(result.customHighlight.TODO.icon, 'check'); + assert.strictEqual(result.customHighlight.TODO.scheme, 'neon+glass'); + assert.strictEqual(result.defaultHighlight.background, '#5C6BC0'); + assert.strictEqual(result.defaultHighlight.icon, 'tag'); + assert.strictEqual(result.defaultHighlight.rulerLane, 'center'); +}); + +QUnit.test('applySchemeToAllTags with none clears scheme from all entries and defaultHighlight', function (assert) { + var customHighlight = { + TODO: { scheme: 'glass', background: '#123456' }, + FIXME: { scheme: 'neon+glass', background: '#9C27B0' } + }; + var defaultHighlight = { background: '#5C6BC0', scheme: 'neon+glass' }; + + var result = highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, 'none'); + + assert.notOk('scheme' in result.customHighlight.TODO, 'TODO scheme removed'); + assert.notOk('scheme' in result.customHighlight.FIXME, 'FIXME scheme removed'); + assert.strictEqual(result.customHighlight.TODO.background, '#123456', 'other props preserved when clearing'); + assert.notOk('scheme' in result.defaultHighlight, 'defaultHighlight scheme removed'); + assert.strictEqual(result.defaultHighlight.background, '#5C6BC0'); +}); + +QUnit.test('applySchemeToAllTags does not mutate its inputs', function (assert) { + var customHighlight = { TODO: { scheme: 'glass', background: '#123456' } }; + var defaultHighlight = { scheme: 'neon+glass' }; + + var result = highlightScheme.applySchemeToAllTags(customHighlight, defaultHighlight, 'neon'); + + assert.strictEqual(customHighlight.TODO.scheme, 'glass', 'input customHighlight unchanged'); + assert.strictEqual(defaultHighlight.scheme, 'neon+glass', 'input defaultHighlight unchanged'); + assert.notStrictEqual(result.customHighlight, customHighlight, 'returns a new customHighlight object'); + assert.notStrictEqual(result.customHighlight.TODO, customHighlight.TODO, 'returns new nested entry objects'); +}); + +QUnit.test('applySchemeToAllTags handles missing config safely', function (assert) { + var result = highlightScheme.applySchemeToAllTags(undefined, undefined, 'neon'); + + assert.deepEqual(result.customHighlight, {}); + assert.strictEqual(result.defaultHighlight.scheme, 'neon'); +});