Skip to content

Commit dbf7f2a

Browse files
committed
feat: enhance VS Code extension with sidebar redesign, improved API integration, and localization updates
1 parent 79a918c commit dbf7f2a

6 files changed

Lines changed: 316 additions & 82 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4444
- **Frontend accessibility pass** — added "Skip to main content" link, global `:focus-visible` styles, ARIA live announcer for notifications, navigation landmark labels, `aria-current="page"`, icon-only button labels, dialog roles/modal attributes, and dynamic HTML `lang` updates.
4545
- **Local API server governance** — hardened all API server routes to enforce widget-scoped API token scopes and reject unscoped or revoked tokens.
4646
- **Offline test harness scripts** — added `scripts/offline-journey-tests.sh` and `scripts/migration-rehearsal.sh` for local critical-journey and migration validation.
47+
- **VS Code extension sidebar home page redesign** — added connection status badge, focus-mode indicator, today's VS Code time card, language/project breakdown with progress bars, top desktop apps, and cleaner action buttons; sidebar API calls now include `X-Api-Token` and `X-Client-Id` headers for local API governance compatibility.
4748

4849
### Changed
4950

@@ -73,6 +74,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7374

7475
### Fixed
7576

77+
- **Settings excluded apps could not be unchecked after saving** — fixed a path-normalization mismatch: the backend stores ignored app paths lowercased, but the frontend compared original-case paths, causing excluded apps to vanish from the list. The list now renders ignored apps directly with case-insensitive matching so they remain visible and togglable.
78+
- **VS Code extension sidebar title showed raw `%timelens.homeView.name%`** — fixed invalid JSON in `vscode-extension/package.nls.json` and `vscode-extension/package.nls.zh-CN.json` (missing comma after `timelens.apiToken.description`), which prevented VS Code from resolving all `%...%` placeholder strings in `package.json`.
7679
- **Profile switching reliability** — fixed a bug where switching profiles could result in a "connection refused" error or missing profiles because `current_profile_id` was stored inside the encrypted profile database.
7780
- **Widget permission dialog state** — fixed install-time permission dialog state reset and instance targeting so permission grants are correctly associated with the widget being installed.
7881
- **Database encryption shutdown corruption** — fixed a critical bug where shutdown re-encryption generated a fresh nonce/salt but did not update the stored metadata, causing the next startup to fail with "Failed to decrypt database". Re-encryption now writes updated metadata and uses atomic temp-file writes.

src/pages/Settings/index.tsx

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,12 @@ export default function Settings() {
304304

305305
// Auto-seed TimeLens exclusion on first run
306306
api.getIgnoredApps().then((ignored) => {
307-
setIgnoredAppsState(ignored);
308-
if (ignored.length === 0 && excludeTimelens) {
307+
const normalized = ignored.map(normalizeExePath);
308+
setIgnoredAppsState(normalized);
309+
if (normalized.length === 0 && excludeTimelens) {
309310
const tlExes = recent
310-
.filter((x) => x.exe_path.toLowerCase().includes("timelens"))
311-
.map((x) => x.exe_path);
311+
.filter((x) => normalizeExePath(x.exe_path).includes("timelens"))
312+
.map((x) => normalizeExePath(x.exe_path));
312313
if (tlExes.length > 0) {
313314
setIgnoredAppsState(tlExes);
314315
api.setIgnoredApps(tlExes).catch(() => {});
@@ -343,21 +344,24 @@ export default function Settings() {
343344
return 0;
344345
};
345346

347+
const normalizeExePath = (p: string) => p.trim().replace(/\//g, "\\").toLowerCase();
348+
346349
const toggleIgnoredApp = (exePath: string) => {
350+
const normalized = normalizeExePath(exePath);
347351
setIgnoredAppsState((prev) =>
348-
prev.includes(exePath)
349-
? prev.filter((p) => p !== exePath)
350-
: [...prev, exePath]
352+
prev.includes(normalized)
353+
? prev.filter((p) => p !== normalized)
354+
: [...prev, normalized]
351355
);
352356
};
353357

354358
const addPickedExcludedApp = (appName: string, exePath: string) => {
355359
if (!exePath) return;
356-
const normalized = exePath.replace(/\//g, "\\");
360+
const normalized = normalizeExePath(exePath);
357361
setIgnoredAppsState((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
358362
setExecutableOptions((prev) => {
359-
if (prev.some((x) => x.exe_path === normalized)) return prev;
360-
return [{ app_name: appName, exe_path: normalized }, ...prev];
363+
if (prev.some((x) => normalizeExePath(x.exe_path) === normalized)) return prev;
364+
return [{ app_name: appName, exe_path: exePath }, ...prev];
361365
});
362366
setExcludePickerValue("");
363367
};
@@ -1197,15 +1201,15 @@ export default function Settings() {
11971201
setExcludeTimelens(next);
11981202
// Add/remove TimeLens exes from ignored list
11991203
const tlExes = executableOptions
1200-
.filter((x) => x.exe_path.toLowerCase().includes("timelens"))
1201-
.map((x) => x.exe_path);
1204+
.filter((x) => normalizeExePath(x.exe_path).includes("timelens"))
1205+
.map((x) => normalizeExePath(x.exe_path));
12021206
if (next) {
12031207
const merged = Array.from(new Set([...ignoredApps, ...tlExes]));
12041208
setIgnoredAppsState(merged);
12051209
api.setIgnoredApps(merged).catch(() => {});
12061210
} else {
12071211
const filtered = ignoredApps.filter(
1208-
(p) => !p.toLowerCase().includes("timelens")
1212+
(p) => !normalizeExePath(p).includes("timelens")
12091213
);
12101214
setIgnoredAppsState(filtered);
12111215
api.setIgnoredApps(filtered).catch(() => {});
@@ -1523,36 +1527,47 @@ export default function Settings() {
15231527
options={executableOptions}
15241528
placeholder={t("data.searchExe")}
15251529
value={excludePickerValue}
1530+
excludePaths={new Set(
1531+
executableOptions
1532+
.filter((x) => ignoredApps.includes(normalizeExePath(x.exe_path)))
1533+
.map((x) => x.exe_path)
1534+
)}
15261535
onChange={(appName, exePath) => {
15271536
if (exePath) { addPickedExcludedApp(appName, exePath); }
15281537
else { setExcludePickerValue(appName); }
15291538
}}
15301539
/>
15311540
<div className="max-h-48 overflow-y-auto rounded-lg border border-surface-border divide-y divide-surface-border">
1532-
{executableOptions.filter((x) => ignoredApps.includes(x.exe_path)).map((row) => {
1533-
const checked = ignoredApps.includes(row.exe_path);
1541+
{ignoredApps.map((path) => {
1542+
const option = executableOptions.find(
1543+
(x) => normalizeExePath(x.exe_path) === path
1544+
);
1545+
const appName =
1546+
option?.app_name ??
1547+
path.split(/[\\/]/).pop()?.replace(/\.exe$/i, "") ??
1548+
path;
1549+
const exePath = option?.exe_path ?? path;
15341550
return (
15351551
<label
1536-
key={row.exe_path}
1552+
key={path}
15371553
className="flex items-start gap-2 px-3 py-2 text-xs text-text-secondary hover:bg-surface-hover cursor-pointer"
15381554
>
15391555
<input
15401556
type="checkbox"
15411557
className="ui-checkbox mt-0.5"
1542-
checked={checked}
1543-
onChange={() => toggleIgnoredApp(row.exe_path)}
1558+
checked={true}
1559+
onChange={() => toggleIgnoredApp(path)}
15441560
/>
15451561
<span className="min-w-0">
1546-
<span className="block text-text-primary truncate">{row.app_name}</span>
1547-
<span className="block text-text-muted truncate" title={row.exe_path}>
1548-
{row.exe_path}
1562+
<span className="block text-text-primary truncate">{appName}</span>
1563+
<span className="block text-text-muted truncate" title={exePath}>
1564+
{exePath}
15491565
</span>
15501566
</span>
15511567
</label>
15521568
);
15531569
})}
1554-
1555-
{executableOptions.filter((x: ExecutableOption) => ignoredApps.includes(x.exe_path)).length === 0 && (
1570+
{ignoredApps.length === 0 && (
15561571
<p className="px-3 py-3 text-xs text-text-muted">{t("data.noExcludedApps")}</p>
15571572
)}
15581573
</div>

vscode-extension/package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"timelens.enabled.description": "Enable local VS Code usage tracking for TimeLens.",
44
"timelens.apiBaseUrl.description": "Local TimeLens API base URL.",
55
"timelens.bridgeKey.description": "Extension bridge key for authenticating with TimeLens local API. Get this from Settings > Local API / Extension Bridge.",
6-
"timelens.apiToken.description": "Local API token for authenticating with TimeLens when token-required mode is enabled. Get this from Settings > Local API / Tokens."
6+
"timelens.apiToken.description": "Local API token for authenticating with TimeLens when token-required mode is enabled. Get this from Settings > Local API / Tokens.",
77
"timelens.flushIntervalSeconds.description": "How often to flush queued sessions to TimeLens API.",
88
"timelens.idleThresholdSeconds.description": "Split a session when editor activity is idle for this many seconds.",
99
"timelens.trackingLevel.description": "How much data to record per VS Code session.",

vscode-extension/package.nls.zh-CN.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"timelens.enabled.description": "启用 TimeLens 本地 VS Code 使用时长追踪。",
44
"timelens.apiBaseUrl.description": "TimeLens 本地 API 基础 URL。",
55
"timelens.bridgeKey.description": "用于向 TimeLens 本地 API 认证的扩展桥接密钥,可在「设置 > 本地 API / 扩展桥接」中获取。",
6-
"timelens.apiToken.description": "在启用「需要 API Token」模式时,用于向 TimeLens 本地 API 认证的令牌,可在「设置 > 本地 API / 令牌」中获取。"
6+
"timelens.apiToken.description": "在启用「需要 API Token」模式时,用于向 TimeLens 本地 API 认证的令牌,可在「设置 > 本地 API / 令牌」中获取。",
77
"timelens.flushIntervalSeconds.description": "将排队会话刷新到 TimeLens API 的频率(秒)。",
88
"timelens.idleThresholdSeconds.description": "当编辑器活动空闲超过此秒数时,将当前会话拆分。",
99
"timelens.trackingLevel.description": "每个 VS Code 会话记录的数据量。",

0 commit comments

Comments
 (0)