feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(desktop): check for updates when the window regains focus - #2461

Merged
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus
Aug 7, 2026
Merged

feat(desktop): check for updates when the window regains focus#2461
jackwener merged 1 commit into
mainfrom
feat/update-check-on-focus

Conversation

@jackwener

Copy link
Copy Markdown
Member

task #162。owner 反馈「自动升级不会及时出现」。

为什么

检查本身没坏,节奏不对:启动 10 秒查一次,之后每 4 小时一次。应用一直开着跨过一次发布,就得等下一个 4 小时刻度才发现——这就是「不及时」的真身。

窗口重新聚焦是个免费信号,而且含义正好:用户回来了。既是发现新版本最有价值的时刻,也是重启提示最不打扰的时刻。

规格六条的落法

  1. 触发点app.on('browser-window-focus')(在 app-lifecycle.ts 里,紧挨 updateService.start())。多窗口触发同一事件也无所谓,被节流吃掉。
  2. 节流 15 分钟,且只有一处记录lastCheckStartedAt真正发起检查的那条路径写入,定时和聚焦共用。按触发路径各记各的会让聚焦检查在定时检查后几秒又发一次——这正是规格点名要避免的。
  3. 4 小时兜底保留,聚焦不重置定时:两条触发独立,靠共享节流去重(采纳规格里"不重置更简单"的判断)。
  4. 重入保护沿用不绕开checkForUpdatesOnFocus 走既有 checkForUpdates()不传 allowDuringDownload——正在下载就是更新已经在来的路上,此时重查正是那个守卫存在的理由。
  5. 测试:新增 5 条(clock.now 注入时间源)。
  6. 无 UI、无 copy 变更,纯 main 侧。

怎么验证的

  • 新增 5 条单测:聚焦触发一次;节流窗口内(15min − 1ms)不重复;跨过窗口后再查;start() 之前不查;dispose() 之后不查。
  • 故障注入:把节流判断整段拆掉 → 恰好 1 条失败(窗口内重复那条)→ 装回全绿。
  • @maka/desktop1804 passed / 0 failed;typecheck / lint / format 全绿。
  • 按范围化测试规矩,只跑了 desktop 这一个 workspace。

一处实现说明

clock 依赖原本只有 setTimeout / clearTimeout,节流需要时间源,所以加了可选的 now?(),默认 Date.now。沿用既有 DI 结构,没有引入新的注入机制。

The updater checked once ten seconds after launch and then every four hours.
An app left open across a release therefore learned about it whenever the next
tick happened to land, which is what "the update does not show up in time"
was — the checks were working, the rhythm was wrong.
Focus is the signal that costs nothing and means the user is here: it is both
when a new version is worth discovering and when a restart prompt is least
disruptive. The four-hour timer stays as the floor for a window nobody comes
back to, and the two triggers stay independent — a focus check deliberately
does not reset the schedule, because the shared throttle already stops them
doubling up.
The throttle is the point. Focus fires constantly, so one shared
`lastCheckStartedAt` is recorded by whichever path actually starts a check,
and a focus check inside 15 minutes of the previous one does nothing.
Recording it per-trigger would let a focus check fire seconds after a
scheduled one. Nothing else moves: silent download and the task-aware install
gate are untouched, and the existing in-flight guards are reused rather than
bypassed.
Behaviour is unchanged in an unpackaged build, before start(), and after
dispose().
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:聚焦触发更新检查 + 15 分钟节流落地——「谁真发起谁写 lastCheckAt」的单处记录实现正确;不传 allowDuringDownload 的守卫目的论证成立(下载中重查违背守卫存在的理由);5 条单测覆盖含 start 前/dispose 后边界,故障注入验证节流会咬。首轮 e2e 失败判 flake 双证据齐:update service 有 isPackaged 守卫、e2e 未打包环境聚焦触发为空操作(因果路径为零)+ 重跑全绿。CI 11 项 pass。合入。

@jackwener
jackwener merged commit 6af9ebd into mainAug 7, 2026
21 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener