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,121 @@
// Rolling ring of recent pen / arbiter events for diagnostic export.
class PenEventRecord {
PenEventRecord({
required this.at,
required this.kind,
required this.pointerId,
this.pressure,
this.tiltX,
this.tiltY,
this.barrel = false,
this.eraser = false,
this.inverted = false,
this.decision,
this.note,
});
final DateTime at;
final String kind; // down|move|up|hw|arbiter
final int pointerId;
final double? pressure;
final double? tiltX;
final double? tiltY;
final bool barrel;
final bool eraser;
final bool inverted;
final String? decision; // draw|pan|reject
final String? note;
Map<String, Object?> toJson() => {
'at': at.toIso8601String(),
'kind': kind,
'pointerId': pointerId,
if (pressure != null) 'pressure': pressure,
if (tiltX != null) 'tiltX': tiltX,
if (tiltY != null) 'tiltY': tiltY,
'barrel': barrel,
'eraser': eraser,
'inverted': inverted,
if (decision != null) 'decision': decision,
if (note != null) 'note': note,
};
}
class PenEventRing {
PenEventRing._();
static final PenEventRing instance = PenEventRing._();
static const int capacity = 2000;
final List<PenEventRecord> _events = <PenEventRecord>[];
void add(PenEventRecord event) {
_events.add(event);
if (_events.length > capacity) {
_events.removeRange(0, _events.length - capacity);
}
}
void recordPointer({
required String kind,
required int pointerId,
required String deviceKind,
double? pressure,
String? decision,
String? note,
}) {
add(PenEventRecord(
at: DateTime.now(),
kind: kind,
pointerId: pointerId,
pressure: pressure,
decision: decision,
note: note ?? deviceKind,
));
}
void recordHardware({
required bool barrel,
required bool eraser,
required bool inverted,
required double tiltX,
required double tiltY,
}) {
add(PenEventRecord(
at: DateTime.now(),
kind: 'hw',
pointerId: -1,
barrel: barrel,
eraser: eraser,
inverted: inverted,
tiltX: tiltX,
tiltY: tiltY,
));
}
void recordArbiter({
required int activeCount,
required String deviceKind,
required bool draw,
required bool fingerDrawing,
}) {
add(PenEventRecord(
at: DateTime.now(),
kind: 'arbiter',
pointerId: -1,
decision: draw ? 'draw' : 'pan',
note: 'count=$activeCount kind=$deviceKind finger=$fingerDrawing',
));
}
List<PenEventRecord> recent({Duration? window}) {
if (window == null) return List.unmodifiable(_events);
final cut = DateTime.now().subtract(window);
return _events.where((e) => e.at.isAfter(cut)).toList(growable: false);
}
List<Map<String, Object?>> toJsonList({Duration? window}) =>
recent(window: window).map((e) => e.toJson()).toList(growable: false);
void clear() => _events.clear();
}