Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- About dialog becomes the update/link/feedback portal on every platform:
User Manual and Changelog links join Website and Report an Issue, and a
diagnostics block (release identity, platform, database and config
paths) carries a Copy Diagnostics button so a bug report can include
the full context verbatim (#58).
- Release-kind acceptance: the update chain — signed-feed rejection,
download cancellation, the N→N+1 lifecycle, data preservation, and the
Linux AppImage in-place swap — is now an executable `release`
Expand Down
42 changes: 42 additions & 0 deletions docs/UX-BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,45 @@ Scheduled(有日期的)。
- Manual acceptance: move a task from 工作 to 生活, confirm it disappears from
工作, appears in 生活 and keeps all other fields; then restart and verify
ownership persists. Exercise both languages and all smart-list views.

## Raycast 对照(商业标杆,2026-10-23 实拆 2.9.0.0)

> 对象:Raycast(macOS 启动器,收费商业软件)。实拆官方 DMG:Info.plist、
> 自研更新器(Updater/UpdaterDaemon,无 Sparkle)、IPC 面、Settings 前端
> 包、InternetAccessPolicy.plist,对照 manual.raycast.com。结论按
> 「学 / 参考 / 不学」记录,落地见 PRODUCT-SPEC §8 与对应切片。

### 学到的(已落地或已排期)

- **About = 更新/链接/反馈门户**:Raycast 的 Settings → About 收口
Raycast Updates、Open Changelog、Website、社区、Send Feedback 五个入口。
Taskly 采纳:About 增加 User Manual 与 Changelog 链接(Website/Report an
Issue 已有),构成「更新(菜单)+ 手册 + 日志 + 反馈」门户。
- **复制诊断信息**:Raycast IPC 有 CopyLogFiles。Taskly 采纳:About 增加
诊断块(版本/平台/架构/OS/DB 路径/配置路径,英文固定标签,可选中)+
「复制诊断信息」按钮,粘贴进 issue 即可带全上下文。
- **网络透明度**:Raycast 随包分发 InternetAccessPolicy.plist,按主机声明
用途与拒绝后果(Little Snitch 等防火墙直接呈现给用户)。Taskly 采纳为
安装链切片:声明 github.com 更新源——把「本地优先、只连更新源」的信任
故事写进包里。
- **更新行为显性化**:Raycast 把更新入口明示在 About。Taskly 排期:设置页
增加「自动检查更新」开关(默认开,写 config),把已有的 4h 静默检查
显性化;下载后仍确认再装(与 Raycast 的退出静默安装分叉——任务管理器
是数据工具,保留用户控制感)。
- **手册结构**:manual.raycast.com 的 Keyboard Shortcuts / Import &
Export / Troubleshooting 独立成章、changelog 挂首页。Taskly 排期并入
手册切片:快捷键全表(§8 面向用户版)与「数据在哪/如何备份」章节。

### 参考但不照搬

- 退出时静默自动安装(CanShutDownForAutoInstall):不采纳,理由同上。
- Sentry 崩溃上报内置于包:v1 无遥测、crash 仅 opt-in,按自己的清单走。
- 社区/Slack 入口:单机 + 云盘同步路线,GitHub issues 即反馈渠道。
- 更新后一次性亮点提示(changelog 一等公民):条件比 Raycast 更好
(结构化 CHANGELOG 自动进 release),列为后续增量切片。

### 明确不学

账号/Store/云同步/AI(与本地优先正面冲突);纯英文零本地化(双语是
Taskly 契约优势);设置页做 WebKit 壳(宿主原生 + i18n 单源);macOS 26
最低版本门槛(14+ 是真实覆盖面差异)。
47 changes: 46 additions & 1 deletion linux/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/utsname.h>
#include <ctime>
#include <filesystem>
#include <functional>
Expand Down Expand Up @@ -1813,11 +1814,55 @@ void show_about_dialog() {
gtk_button_set_label(GTK_BUTTON(website), tr("aboutWebsite").c_str());
gtk_widget_set_hexpand(website, FALSE);
gtk_box_append(GTK_BOX(links), website);
auto* manual = gtk_link_button_new("https://taskly.jrtx.site/manual.html");
gtk_button_set_label(GTK_BUTTON(manual), tr("aboutManual").c_str());
gtk_widget_set_hexpand(manual, FALSE);
gtk_box_append(GTK_BOX(links), manual);
auto* links_row2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 24);
gtk_widget_set_halign(links_row2, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(content), links_row2);
auto* changelog = gtk_link_button_new(
"https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md");
gtk_button_set_label(GTK_BUTTON(changelog), tr("aboutChangelog").c_str());
gtk_widget_set_hexpand(changelog, FALSE);
gtk_box_append(GTK_BOX(links_row2), changelog);
auto* issue = gtk_link_button_new(
"https://github.com/turinglambdaai/taskly/issues");
gtk_button_set_label(GTK_BUTTON(issue), tr("aboutReportIssue").c_str());
gtk_widget_set_hexpand(issue, FALSE);
gtk_box_append(GTK_BOX(links), issue);
gtk_box_append(GTK_BOX(links_row2), issue);
// Diagnostics block (PRODUCT-SPEC §8): the release identity and the
// ~/.taskly contract paths a bug report starts from. Labels are English
// fixed strings — machine-oriented like the CLI's --json.
static std::string diagnostics = [] {
utsname names{};
uname(&names);
return std::string("Taskly v") + rivet_app::kVersion + " (" +
names.sysname + " " + names.release + ", " + names.machine + ")" +
"\nDatabase: " + g_get_home_dir() + "/.taskly/tasks.db" +
"\nConfig: " + g_get_home_dir() + "/.taskly/config.ini";
}();
auto* diagnostics_label = gtk_label_new(diagnostics.c_str());
gtk_label_set_selectable(GTK_LABEL(diagnostics_label), TRUE);
gtk_label_set_justify(GTK_LABEL(diagnostics_label), GTK_JUSTIFY_LEFT);
PangoAttrList* mono = pango_attr_list_new();
pango_attr_list_insert(mono, pango_attr_family_new("monospace"));
gtk_label_set_attributes(GTK_LABEL(diagnostics_label), mono);
pango_attr_list_unref(mono);
gtk_widget_set_halign(diagnostics_label, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(content), diagnostics_label);
auto* copy_diagnostics = gtk_button_new_with_label(
tr("aboutCopyDiagnostics").c_str());
gtk_widget_set_halign(copy_diagnostics, GTK_ALIGN_CENTER);
g_signal_connect(
copy_diagnostics, "clicked",
G_CALLBACK(+[](GtkButton*, gpointer user_data) {
auto* text = static_cast<const std::string*>(user_data);
auto* clipboard = gdk_display_get_clipboard(gdk_display_get_default());
gdk_clipboard_set_text(clipboard, text->c_str());
}),
&diagnostics);
gtk_box_append(GTK_BOX(content), copy_diagnostics);
g_signal_connect(
dialog, "response",
G_CALLBACK(+[](GtkDialog* dialog, gint, gpointer) {
Expand Down
50 changes: 49 additions & 1 deletion macos-host/Sources/RivetHost/Sheets.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import SwiftUI

/// Signed ARGB int as stored in the DB color column (DATA-FORMAT §2):
Expand Down Expand Up @@ -244,7 +245,8 @@ private struct ColorSwatchGrid: View {
}
}

/// About box (Help ▸ About) — `Taskly v<version>` (PRODUCT-SPEC §8).
/// About box (Help ▸ About) — the update/link/feedback hub and the
/// diagnostics surface (PRODUCT-SPEC §8).
struct AboutSheet: View {
@Environment(AppModel.self) private var model
@Environment(\.dismiss) private var dismiss
Expand All @@ -254,6 +256,34 @@ struct AboutSheet: View {
?? RivetGeneratedConfig.version
}

/// The diagnostics block a bug report starts from (PRODUCT-SPEC §8):
/// release identity plus the `~/.taskly` contract paths. Labels are
/// English fixed strings — machine-oriented like the CLI's --json.
private var diagnosticsText: String {
let os = ProcessInfo.processInfo.operatingSystemVersion
let arch: String
#if arch(arm64)
arch = "arm64"
#elseif arch(x86_64)
arch = "x86_64"
#else
arch = "unknown"
#endif
let home = FileManager.default.homeDirectoryForCurrentUser.path
return """
Taskly \(version) (macOS \(os.majorVersion).\(os.minorVersion), \(arch))
Database: \(home)/.taskly/tasks.db
Config: \(home)/.taskly/config.ini
"""
}

private func copyDiagnostics() {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(diagnosticsText, forType: .string)
model.flashStatus(model.t("statusDiagnosticsCopied"))
}

var body: some View {
VStack(spacing: 12) {
Image(systemName: "checklist")
Expand All @@ -275,11 +305,29 @@ struct AboutSheet: View {
HStack(spacing: 20) {
Link(model.t("aboutWebsite"),
destination: URL(string: "https://taskly.jrtx.site")!)
Link(model.t("aboutManual"),
destination: URL(string: "https://taskly.jrtx.site/manual.html")!)
}
.font(.system(size: 12))
.tint(model.theme.accent)
HStack(spacing: 20) {
Link(model.t("aboutChangelog"),
destination: URL(string: "https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md")!)
Link(model.t("aboutReportIssue"),
destination: URL(string: "https://github.com/turinglambdaai/taskly/issues")!)
}
.font(.system(size: 12))
.tint(model.theme.accent)
Text(diagnosticsText)
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(model.theme.secondaryText)
.textSelection(.enabled)
.multilineTextAlignment(.leading)
.lineLimit(3)
Button(model.t("aboutCopyDiagnostics")) { copyDiagnostics() }
.font(.system(size: 12))
.buttonStyle(.bordered)
.tint(model.theme.accent)
Button(model.t("dialogConfirm")) { dismiss() }
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
Expand Down
4 changes: 4 additions & 0 deletions shared/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,9 @@
"aboutLicense": "Released under the GNU AGPL-3.0",
"aboutWebsite": "Website",
"aboutReportIssue": "Report an Issue",
"aboutManual": "User Manual",
"aboutChangelog": "Changelog",
"aboutCopyDiagnostics": "Copy Diagnostics",
"statusDiagnosticsCopied": "Diagnostics copied — paste it into your issue",
"updateCopyError": "Copy error"
}
4 changes: 4 additions & 0 deletions shared/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,9 @@
"aboutLicense": "以 GNU AGPL-3.0 授权发布",
"aboutWebsite": "访问官网",
"aboutReportIssue": "问题反馈",
"aboutManual": "使用手册",
"aboutChangelog": "更新日志",
"aboutCopyDiagnostics": "复制诊断信息",
"statusDiagnosticsCopied": "已复制诊断信息,可直接粘贴到 issue",
"updateCopyError": "复制错误信息"
}
20 changes: 14 additions & 6 deletions shared/spec/PRODUCT-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,20 @@ pickers. Save (blank text silently ignored), Delete (confirm first), Cancel
Check for Updates… (manual online-update check; auto-check runs at
launch and every 4 h — contract: `UPDATE.md`).
- Help: About — `Taskly v<version>` + `© 2026 Taskly Team` +
`aboutContent` + license line (`aboutLicense`) + two link buttons on
every host: Website (`aboutWebsite` → taskly.jrtx.site) and Report an
Issue (`aboutReportIssue` → the GitHub issue tracker). macOS and
Windows render the fields inside their sheet/dialog; Linux builds the
same layout on a plain GtkDialog — GtkAboutDialog cannot carry a
second link, so the native about dialog is not used.
`aboutContent` + license line (`aboutLicense`) + link buttons on every
host: Website (`aboutWebsite` → taskly.jrtx.site), User Manual
(`aboutManual` → taskly.jrtx.site/manual.html), Changelog
(`aboutChangelog` → the CHANGELOG on GitHub), and Report an Issue
(`aboutReportIssue` → the GitHub issue tracker). Below the links, a
diagnostics block shows the release identity and the data locations —
app version, platform + architecture, OS version, database path,
config path — with a `aboutCopyDiagnostics` button that puts the same
block on the clipboard so a bug report can include it verbatim; the
copy confirmation is a transient status message. Diagnostics labels
are English fixed strings (machine-oriented, like the CLI's `--json`).
macOS and Windows render the fields inside their sheet/dialog; Linux
builds the same layout on a plain GtkDialog — GtkAboutDialog cannot
carry multiple links, so the native about dialog is not used.
- All accelerators work window-wide without opening the menu.

## 9. Reminders (all platforms)
Expand Down
64 changes: 64 additions & 0 deletions windows/MainWindow.xaml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2454,16 +2454,80 @@ winrt::fire_and_forget MainWindow::OnAbout(
L"https://taskly.jrtx.site"});
website.HorizontalAlignment(mx::HorizontalAlignment::Center);
website.Padding({0, 0, 0, 0});
auto manual = mxc::HyperlinkButton{};
manual.Content(winrt::box_value(t("aboutManual")));
manual.NavigateUri(winrt::Windows::Foundation::Uri{
L"https://taskly.jrtx.site/manual.html"});
manual.HorizontalAlignment(mx::HorizontalAlignment::Center);
manual.Padding({0, 0, 0, 0});
auto changelog = mxc::HyperlinkButton{};
changelog.Content(winrt::box_value(t("aboutChangelog")));
changelog.NavigateUri(winrt::Windows::Foundation::Uri{
L"https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md"});
changelog.HorizontalAlignment(mx::HorizontalAlignment::Center);
changelog.Padding({0, 0, 0, 0});
auto issue = mxc::HyperlinkButton{};
issue.Content(winrt::box_value(t("aboutReportIssue")));
issue.NavigateUri(winrt::Windows::Foundation::Uri{
L"https://github.com/turinglambdaai/taskly/issues"});
issue.HorizontalAlignment(mx::HorizontalAlignment::Center);
issue.Padding({0, 0, 0, 0});
// Diagnostics block (PRODUCT-SPEC §8): the release identity and the
// ~/.taskly contract paths a bug report starts from. Labels are English
// fixed strings — machine-oriented like the CLI's --json.
std::wstring profile;
wchar_t profile_buf[MAX_PATH];
const DWORD profile_len = GetEnvironmentVariableW(
L"USERPROFILE", profile_buf, MAX_PATH);
if (profile_len > 0 && profile_len < MAX_PATH) {
profile.assign(profile_buf, profile_len);
}
const std::wstring diagnostics =
std::wstring(L"Taskly v") + wide(rivet_app::kVersion) +
L" (Windows, x64)\nDatabase: " + profile + L"\\.taskly\\tasks.db" +
L"\nConfig: " + profile + L"\\.taskly\\config.ini";
auto diagnostics_text = mxc::TextBlock{};
diagnostics_text.Text(diagnostics);
diagnostics_text.FontFamily(mx::Media::FontFamily{L"Consolas"});
diagnostics_text.FontSize(11);
diagnostics_text.Margin({0, 8, 0, 0});
diagnostics_text.IsTextSelectionEnabled(true);
auto copy_diagnostics = mxc::Button{};
copy_diagnostics.Content(winrt::box_value(t("aboutCopyDiagnostics")));
copy_diagnostics.HorizontalAlignment(mx::HorizontalAlignment::Center);
copy_diagnostics.Padding({12, 4, 12, 4});
copy_diagnostics.Click(
[text = winrt::weak_ref<mxc::Button>(copy_diagnostics), diagnostics](
auto&&, auto&&) {
auto package =
winrt::Windows::ApplicationModel::DataTransfer::DataPackage{};
package.RequestedOperation(
winrt::Windows::ApplicationModel::DataTransfer::
DataPackageOperation::Copy);
package.SetText(diagnostics);
winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent(
package);
if (auto button = text.get()) {
button.Content(winrt::box_value(t("statusDiagnosticsCopied")));
auto restore = mx::DispatcherTimer();
restore.Interval(std::chrono::milliseconds{2500});
restore.Tick([restore, text](auto&&, auto&&) {
if (auto b = text.get()) {
b.Content(winrt::box_value(t("aboutCopyDiagnostics")));
}
restore.Stop();
});
restore.Start();
}
});
panel.Children().Append(tagline);
panel.Children().Append(license);
panel.Children().Append(website);
panel.Children().Append(manual);
panel.Children().Append(changelog);
panel.Children().Append(issue);
panel.Children().Append(diagnostics_text);
panel.Children().Append(copy_diagnostics);

auto dialog = mxc::ContentDialog{};
dialog.Title(winrt::box_value(L"Taskly v" + wide(rivet_app::kVersion)));
Expand Down
Loading