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