feat: unified shell, diagnostics pack, native Office, sticky board
All checks were successful
CI / Windows build (push) Successful in 14m22s

Make Surface remote debugging and classroom workflows viable: always-on
structured logs with one-click zip export, a single AppShell chrome,
OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky
board. Also drop spike/legacy ink widgets and tighten pen feel
(predictor, PenInfoHistory, page-tile layer).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 17:55:27 +08:00
parent 3cabc7e074
commit d346cc2670
49 changed files with 2883 additions and 2515 deletions

View File

@@ -0,0 +1,138 @@
// Build a zip diagnostic pack the user can hand back for remote debugging.
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import '../editor/canvas/input_diagnostics.dart';
import '../editor/input/pen_input_service.dart';
import 'badnote_log.dart';
import 'frame_sampler.dart';
import 'pen_event_ring.dart';
class DiagnosticExportResult {
DiagnosticExportResult({required this.zipPath, required this.bytes});
final String zipPath;
final int bytes;
}
class DiagnosticExport {
DiagnosticExport._();
static final DiagnosticExport instance = DiagnosticExport._();
/// Flush logs and write a zip under Documents/badnote_diagnostics/.
Future<DiagnosticExportResult> exportPack({
Duration penWindow = const Duration(minutes: 5),
}) async {
final log = BadNoteLog.instance;
await log.flush();
final meta = <String, Object?>{
'exportedAt': DateTime.now().toIso8601String(),
'sessionId': log.sessionId,
'platform': Platform.operatingSystem,
'osVersion': Platform.operatingSystemVersion,
'localHostname': Platform.localHostname,
'numberOfProcessors': Platform.numberOfProcessors,
'flutter': {
'foundationDebug': kDebugMode,
'foundationProfile': kProfileMode,
'foundationRelease': kReleaseMode,
},
'penNative': PenInputService.instance.debugSummary,
'penActive': PenInputService.instance.isActive,
'zoom': InputDiagnostics.instance.summary(),
'frames': FrameSampler.instance.summary(),
'instructions':
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip.',
};
final archive = Archive();
void addText(String name, String body) {
final bytes = utf8.encode(body);
archive.addFile(ArchiveFile(name, bytes.length, bytes));
}
addText('meta.json', const JsonEncoder.withIndent(' ').convert(meta));
addText(
'pen_events.json',
const JsonEncoder.withIndent(' ').convert(
PenEventRing.instance.toJsonList(window: penWindow),
),
);
addText(
'frame_samples.json',
const JsonEncoder.withIndent(' ').convert(FrameSampler.instance.toJsonList()),
);
addText(
'log_ring.json',
const JsonEncoder.withIndent(' ').convert(log.snapshotRing()),
);
// Include on-disk session NDJSON if present.
final sessionPath = log.path;
if (sessionPath != null) {
try {
final f = File(sessionPath);
if (await f.exists()) {
final bytes = await f.readAsBytes();
archive.addFile(
ArchiveFile('session.ndjson', bytes.length, bytes),
);
}
} catch (_) {}
}
// Legacy input log if it exists alongside.
try {
Directory dir;
try {
dir = await getApplicationDocumentsDirectory();
} catch (_) {
dir = await getTemporaryDirectory();
}
final legacy = File(
'${dir.path}${Platform.pathSeparator}badnote_input_log.txt',
);
if (await legacy.exists()) {
final bytes = await legacy.readAsBytes();
archive.addFile(
ArchiveFile('legacy_input_log.txt', bytes.length, bytes),
);
}
} catch (_) {}
final encoded = ZipEncoder().encode(archive);
if (encoded.isEmpty) {
throw StateError('Failed to encode diagnostic zip');
}
Directory outDir = log.directory ??
Directory(
'${(await getApplicationDocumentsDirectory()).path}'
'${Platform.pathSeparator}badnote_diagnostics',
);
if (!await outDir.exists()) {
await outDir.create(recursive: true);
}
final stamp = DateTime.now()
.toIso8601String()
.replaceAll(':', '-')
.replaceAll('.', '-');
final zipPath =
'${outDir.path}${Platform.pathSeparator}badnote_diag_$stamp.zip';
await File(zipPath).writeAsBytes(encoded, flush: true);
BadNoteLog.instance.info(
LogSubsystem.diag,
'export_pack',
fields: {'path': zipPath, 'bytes': encoded.length},
);
return DiagnosticExportResult(zipPath: zipPath, bytes: encoded.length);
}
}