You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/02 14:52:11 UTC
(maka) 03/04: feat(desktop): name the update channel in the diagnostics report
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch feat/about-page-hierarchy
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 9b75da3a5ff34e57f4df81a58075b3f349a058a8
Author: AstroHan <le...@outlook.com>
AuthorDate: Wed Sep 2 22:51:36 2026 +0800
feat(desktop): name the update channel in the diagnostics report
The diagnostics report printed `Build: packaged @ <sha>`, which cannot tell a nightly install from a release one — the two channels update from different feeds and reproduce different bugs. Read the channel from the packaged manifest through a total helper that degrades to `unknown` instead of throwing, because the report must still copy when the manifest itself is what broke.
---
.../__tests__/main-process-diagnostics.test.ts | 21 +++
.../__tests__/native-diagnostic-dialog.test.ts | 1 +
apps/desktop/src/main/app-update-attestation.ts | 27 ++-
apps/desktop/src/main/main-process-diagnostics.ts | 11 ++
apps/desktop/src/main/main.ts | 9 +
apps/desktop/src/main/runtime-host-boot.ts | 5 +
packages/ui/src/primitives/page-header.tsx | 191 ---------------------
7 files changed, 72 insertions(+), 193 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
index ec6b42eba7..b96c0114f4 100644
--- a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
+++ b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
@@ -32,11 +32,13 @@ import {
MAIN_PROCESS_DIAGNOSTIC_LOG_MAX_BYTES,
mainProcessLogBuffer,
parseDesktopDiagnosticInput,
+ type DesktopDiagnosticChannel,
} from '../main-process-diagnostics.js';
const environment = {
appVersion: '0.1.8',
buildMode: 'dev' as const,
+ updateChannel: 'dev' as const,
buildCommit: 'a'.repeat(40),
electronVersion: '38.0.0',
nodeVersion: '22.0.0',
@@ -107,6 +109,25 @@ test('formats one redacted Desktop and Runtime Host diagnostic report', () => {
assert.doesNotMatch(report, /sk-secretvalue123|\/home\/tester/);
});
+test('names the update channel, which buildMode alone cannot distinguish', () => {
+ const reportFor = (channel: DesktopDiagnosticChannel, buildMode: 'dev' | 'packaged') =>
+ formatDesktopDiagnosticReport(
+ { surface: 'manual', hostTarget: 'default' },
+ { ...environment, buildMode, updateChannel: channel },
+ [],
+ { ok: false, error: 'not started' },
+ undefined,
+ new Date('2026-08-09T00:00:00Z'),
+ );
+
+ // Both packaged installs report `Build: packaged`; only the channel line
+ // says which feed and which attestation signer they trust.
+ assert.match(reportFor('nightly', 'packaged'), /^Channel: nightly$/mu);
+ assert.match(reportFor('release', 'packaged'), /^Channel: release$/mu);
+ // A checkout follows no feed, so it is neither of them.
+ assert.match(reportFor('dev', 'dev'), /^Channel: dev$/mu);
+});
+
test('bounds renderer diagnostic text and rejects unknown fields', () => {
const input = parseDesktopDiagnosticInput({
surface: 'toast',
diff --git a/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts b/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
index fd5a621e6c..954f0d89b6 100644
--- a/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
+++ b/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
@@ -36,6 +36,7 @@ import {
const diagnosticEnvironment = () => ({
appVersion: '0.1.8',
buildMode: 'packaged' as const,
+ updateChannel: 'release' as const,
buildCommit: null,
electronVersion: '38.0.0',
nodeVersion: '22.0.0',
diff --git a/apps/desktop/src/main/app-update-attestation.ts b/apps/desktop/src/main/app-update-attestation.ts
index bc8a4775ef..bc2d01478f 100644
--- a/apps/desktop/src/main/app-update-attestation.ts
+++ b/apps/desktop/src/main/app-update-attestation.ts
@@ -21,8 +21,8 @@ import { bundleFromJSON, type Bundle } from '@sigstore/bundle';
import { getTrustedRoot } from '@sigstore/tuf';
import { toSignedEntity, toTrustMaterial, Verifier } from '@sigstore/verify';
import { createHash } from 'node:crypto';
-import { createReadStream } from 'node:fs';
-import { basename } from 'node:path';
+import { createReadStream, readFileSync } from 'node:fs';
+import { basename, join } from 'node:path';
const PRODUCT_REPOSITORY = 'apache/maka';
const PRODUCT_RELEASE_WORKFLOW = '.github/workflows/release-cli-finalize.yml';
@@ -80,6 +80,29 @@ export function desktopUpdateChannelFromManifest(manifest: unknown): DesktopUpda
return channel;
}
+/**
+ * The channel a diagnostic report names for this binary.
+ *
+ * Total by construction, unlike `desktopUpdateChannelFromManifest`: a report
+ * must still copy when the manifest is the very thing that is broken, and the
+ * report saying `unknown` is more useful than the copy failing. A checkout
+ * follows no feed at all, so it reports `dev` rather than the updater's
+ * `release` placeholder.
+ */
+export function desktopDiagnosticUpdateChannel(input: {
+ readonly isPackaged: boolean;
+ readonly appPath: string;
+}): DesktopUpdateChannel | 'dev' | 'unknown' {
+ if (!input.isPackaged) return 'dev';
+ try {
+ return desktopUpdateChannelFromManifest(
+ JSON.parse(readFileSync(join(input.appPath, 'package.json'), 'utf8')),
+ );
+ } catch {
+ return 'unknown';
+ }
+}
+
function productWorkflowSigner(channel: DesktopUpdateChannel): RegExp {
const workflow = channel === 'nightly' ? PRODUCT_NIGHTLY_WORKFLOW : PRODUCT_RELEASE_WORKFLOW;
return new RegExp(
diff --git a/apps/desktop/src/main/main-process-diagnostics.ts b/apps/desktop/src/main/main-process-diagnostics.ts
index dba428ab43..a5b39dccdd 100644
--- a/apps/desktop/src/main/main-process-diagnostics.ts
+++ b/apps/desktop/src/main/main-process-diagnostics.ts
@@ -44,9 +44,18 @@ const INPUT_TRUNCATION_MARKER = '\n<diagnostic input truncated>';
const EXECUTION_DIAGNOSTIC_TIMEOUT_MS = 2_000;
export const MAIN_PROCESS_DIAGNOSTIC_LOG_MAX_BYTES = 256 * 1024;
+/**
+ * The release feed a build follows, as a report names it. `buildMode` alone
+ * cannot say this: every packaged install reports `packaged`, so a nightly and
+ * a release looked identical in a report while following different feeds and
+ * different attestation signers. `dev` is not a feed — a checkout follows none.
+ */
+export type DesktopDiagnosticChannel = 'release' | 'nightly' | 'dev' | 'unknown';
+
export interface DesktopDiagnosticEnvironment {
readonly appVersion: string;
readonly buildMode: 'dev' | 'packaged';
+ readonly updateChannel: DesktopDiagnosticChannel;
readonly buildCommit: string | null;
readonly electronVersion: string;
readonly nodeVersion: string;
@@ -63,6 +72,7 @@ export interface DesktopDiagnosticEnvironment {
export interface DesktopDiagnosticEnvironmentSource {
readonly appVersion: string;
readonly buildMode: 'dev' | 'packaged';
+ readonly updateChannel: DesktopDiagnosticChannel;
readonly buildCommit: string | null;
readonly locale: string;
readonly workspacePath: string;
@@ -431,6 +441,7 @@ export function formatDesktopDiagnosticReport(
'Environment',
`Maka: ${environment.appVersion}`,
`Build: ${environment.buildMode}${environment.buildCommit ? ` @ ${environment.buildCommit.slice(0, 12)}` : ''}`,
+ `Channel: ${environment.updateChannel}`,
`Electron: ${environment.electronVersion}`,
`Chrome: ${environment.chromeVersion}`,
`Node: ${environment.nodeVersion}`,
diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts
index 8cea0b9d1d..16133b4515 100644
--- a/apps/desktop/src/main/main.ts
+++ b/apps/desktop/src/main/main.ts
@@ -27,6 +27,7 @@ import { app, clipboard, dialog, ipcMain } from 'electron';
import { join } from 'node:path';
import { resolveBuildInfo } from './build-info.js';
import { resolveUpdateTestUserDataDirectory } from './app-update-test-context.js';
+import { desktopDiagnosticUpdateChannel } from './app-update-attestation.js';
import {
captureDesktopDiagnosticEnvironment,
copyDesktopDiagnosticReport,
@@ -176,6 +177,10 @@ if (!app.requestSingleInstanceLock()) {
captureDesktopDiagnosticEnvironment({
appVersion: app.getVersion(),
buildMode: buildInfo.mode,
+ updateChannel: desktopDiagnosticUpdateChannel({
+ isPackaged: app.isPackaged,
+ appPath: app.getAppPath(),
+ }),
buildCommit: buildInfo.commit,
locale: app.getLocale(),
workspacePath: join(app.getPath('userData'), 'workspaces', 'default'),
@@ -212,6 +217,10 @@ if (!app.requestSingleInstanceLock()) {
captureDesktopDiagnosticEnvironment({
appVersion: app.getVersion(),
buildMode: buildInfo.mode,
+ updateChannel: desktopDiagnosticUpdateChannel({
+ isPackaged: app.isPackaged,
+ appPath: app.getAppPath(),
+ }),
buildCommit: buildInfo.commit,
locale: app.getLocale(),
workspacePath: join(app.getPath('userData'), 'workspaces', 'default'),
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 75e05259fb..e65193172d 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -70,6 +70,7 @@ import { createMcpOAuthController } from "./mcp-oauth-controller.js";
import { registerAppClientIpc, registerAppIpc } from "./app-ipc-main.js";
import { createAppQuitCoordinator } from "./app-quit-coordinator.js";
import {
+ desktopDiagnosticUpdateChannel,
desktopUpdateChannelFromManifest,
verifyDownloadedUpdateAttestation,
} from "./app-update-attestation.js";
@@ -326,6 +327,10 @@ const desktopDiagnostics: DesktopDiagnosticsDeps = {
captureDesktopDiagnosticEnvironment({
appVersion: app.getVersion(),
buildMode: buildInfo.mode,
+ updateChannel: desktopDiagnosticUpdateChannel({
+ isPackaged: app.isPackaged,
+ appPath: app.getAppPath(),
+ }),
buildCommit: buildInfo.commit,
locale: app.getLocale(),
workspacePath: workspaceRoot,
diff --git a/packages/ui/src/primitives/page-header.tsx b/packages/ui/src/primitives/page-header.tsx
deleted file mode 100644
index 28b7b77e35..0000000000
--- a/packages/ui/src/primitives/page-header.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// packages/ui/src/primitives/page-header.tsx
-//
-// PageHeader — the shared header shell for the SETTINGS intro dialect:
-//
-// SETTINGS intros (as='h3', or as='h2' for the About hero): the smaller
-// Permission / Health / About page intro cards — a body-tier
-// semibold title, a lede, and a trailing quieter META cluster
-// (RelativeTime + refresh Button) or a leading feature ICON + trailing
-// BADGE chip. Previously `.settingsPermissionIntro`,
-// `.settingsHealthIntro`, `.settingsAboutHero`.
-//
-// The MODULE-page dialect this primitive also used to serve (the
-// `.maka-module-main-header` hero) is retired: 技能 / MCP / 定时任务 /
-// 每日回顾 moved onto the ModulePage primitive, whose Astryx Layout header
-// owns title + meta + actions.
-//
-// Layout & typography strategy: the shell is styled with portable semantic
-// utilities, but every call site KEEPS its existing wrapper class (passed via
-// `className`) so the wrapper CSS — which already owns the surface chrome
-// (card border/background/radius, flex vs grid layout, gap, mobile
-// breakpoints) and the per-slot typography (`.settingsPermissionIntro h3`,
-// `.settingsAboutHero p`, …) — keeps governing the visuals unchanged.
-// The primitive only converges the STRUCTURE (slot order, title/subtitle/
-// eyebrow/badge/icon/actions/meta arrangement). Slots expose `data-slot`
-// hooks so contracts that used to pin the old `h2`/`p` direct children can
-// re-pin the primitive's slots where DOM structure genuinely moved.
-
-import type { ReactNode } from 'react';
-import { cn } from '../utils.js';
-
-export interface PageHeaderProps {
- /** Title text. Rendered as an <h2> (module scale) or <h3> (settings scale). */
- title: ReactNode;
- /** Optional lede line under the title (muted). */
- subtitle?: ReactNode;
- /**
- * Optional small caps/eyebrow line ABOVE the title (e.g. a section kicker).
- * Rendered muted+semibold before the title row.
- */
- eyebrow?: ReactNode;
- /** Optional leading glyph, rendered in a framed icon box left of the content. */
- icon?: ReactNode;
- /**
- * Optional marker rendered inline right AFTER the title (About
- * version/channel pills). Accepts one node or a fragment.
- */
- badge?: ReactNode;
- /**
- * Title heading level + scale. 'h2' = the About hero scale, 'h3' = the
- * settings intro scale. Defaults to 'h2'. The exact font-size/weight is
- * left to the wrapper CSS (`.settingsAboutHero h2`,
- * `.settingsPermissionIntro h3`, …); this only picks the semantic tag.
- */
- as?: 'h2' | 'h3';
- /** Id applied to the title element (aria-labelledby targets). */
- titleId?: string;
- /**
- * Right-aligned action cluster (buttons, search box). The cluster's own
- * positioning class (e.g. `maka-module-main-actions`, `maka-scheduled-task-top-actions`)
- * is passed as a child by the call site; the shell only slots it to the end.
- */
- actions?: ReactNode;
- /**
- * Trailing quieter cluster — the settings meta stack (RelativeTime +
- * refresh Button + optional read-only badge). Like `actions`, the cluster's
- * own class (`settingsPermissionMeta`, `settingsHealthMeta`) rides on the
- * child; the shell only slots it.
- */
- meta?: ReactNode;
- /** Wrapper class — the existing call-site hook (kept so CSS + contracts stay pinned). */
- className?: string;
- /**
- * Class for the title+subtitle content column (e.g. `maka-scheduled-task-heading`).
- * Lets a call site keep a wrapper that its CSS targets as the heading group.
- */
- contentClassName?: string;
- /** Class for the leading icon box (e.g. `settingsAboutLogo`). */
- iconClassName?: string;
- /** Class for the inline title+badge row (e.g. `settingsAboutHeading`). */
- headingRowClassName?: string;
- /** Class for the subtitle line (e.g. `settingsAboutTagline`), when a call site's CSS targets it. */
- subtitleClassName?: string;
- /** Render the wrapper as a <header> (default) or a <div>. */
- as_wrapper?: 'header' | 'div';
-}
-
-export function PageHeader({
- title,
- subtitle,
- eyebrow,
- icon,
- badge,
- as = 'h2',
- titleId,
- actions,
- meta,
- className,
- contentClassName,
- iconClassName,
- headingRowClassName,
- subtitleClassName,
- as_wrapper = 'header',
-}: PageHeaderProps): ReactNode {
- const Title = as;
- const Wrapper = as_wrapper;
-
- // The title row: the heading, then any inline badge marker. When neither a
- // badge nor a headingRow class is supplied we render the bare heading, so
- // simple call sites (skills/plan/permission/health) keep a plain `h2`/`h3`
- // as the direct descendant their CSS expects.
- const heading = (
- <Title
- id={titleId}
- data-slot="page-header-title"
- // No typography utilities here — the wrapper CSS owns h2/h3 sizing.
- className="maka-page-header-title"
- >
- {title}
- </Title>
- );
- const titleRow =
- badge != null || headingRowClassName ? (
- <div
- data-slot="page-header-heading-row"
- className={cn('maka-page-header-heading-row', headingRowClassName)}
- >
- {heading}
- {badge != null ? badge : null}
- </div>
- ) : (
- heading
- );
-
- const content = (
- <div
- data-slot="page-header-content"
- className={cn('maka-page-header-content', contentClassName)}
- >
- {eyebrow != null ? (
- <p data-slot="page-header-eyebrow" className="maka-page-header-eyebrow">
- {eyebrow}
- </p>
- ) : null}
- {titleRow}
- {subtitle != null ? (
- <p
- data-slot="page-header-subtitle"
- className={cn('maka-page-header-subtitle', subtitleClassName)}
- >
- {subtitle}
- </p>
- ) : null}
- </div>
- );
-
- return (
- <Wrapper data-slot="page-header" className={className}>
- {icon != null ? (
- <span
- data-slot="page-header-icon"
- aria-hidden="true"
- className={iconClassName}
- >
- {icon}
- </span>
- ) : null}
- {content}
- {actions != null ? actions : null}
- {meta != null ? meta : null}
- </Wrapper>
- );
-}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org