Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d627c22
SPEC-46 P1a: the doc index's security boundary and title extraction
leduckhc Aug 9, 2026
a013c07
SPEC-46 P1b: freeze the docs wire contract
leduckhc Aug 9, 2026
fbb4ffc
SPEC-46: correct the mockup's published URL to a capability URL (D9)
leduckhc Aug 9, 2026
0398c1b
SPEC-46 P1c: the docs index, grants, and the publish route (server)
leduckhc Aug 9, 2026
c997331
SPEC-46 D10/D15 rev 2: bind the doc port lazily, and only on the tailnet
leduckhc Aug 9, 2026
60426fe
SPEC-46 P1d: the Docs screen, markdown preview, and publish sheet (app)
leduckhc Aug 9, 2026
3568d08
SPEC-46 P1e: re-index docs when the worktree list arrives
leduckhc Aug 9, 2026
4992ed7
SPEC-46: let the P1e regression test type-check
leduckhc Aug 9, 2026
ec85d44
fix(qa): stop offering --lan as a remedy publishing cannot use
leduckhc Aug 10, 2026
800f829
fix(qa): let the title lead the doc preview, not the front-matter chips
leduckhc Aug 10, 2026
00317d0
feat(docs): index every doc git does not ignore, and make the list na…
leduckhc Aug 10, 2026
55f1b93
docs(mockups): the docs popover searches, scrolls and caps (rev 2)
leduckhc Aug 10, 2026
c10fbb8
fix(qa): unwrap the grant object from docs.publish ack
leduckhc Aug 10, 2026
a84fde7
test(docs): pin the docs.publish ack shape that broke HTML preview
leduckhc Aug 10, 2026
e169f53
docs(spec): D8 rev 2 — where the viewer is decides how HTML opens
leduckhc Aug 10, 2026
8495844
feat(docs): open HTML on the host when the viewer is already there (D…
leduckhc Aug 10, 2026
ed52315
fix(qa): three high-severity bugs from ocr review
leduckhc Aug 10, 2026
7439808
fix(security): use powershell Start-Process instead of cmd /c start o…
leduckhc Aug 10, 2026
20a1cd8
refactor(docs): fix 13 ocr audit findings, restructure scan and listener
leduckhc Aug 10, 2026
07e786b
fix(docs): the bugs the ocr review found, with tests that bite
leduckhc Aug 10, 2026
402bd8b
fix(docs): defensive error handling in scan loop
leduckhc Aug 11, 2026
ff18056
docs: SPEC-46 P1 branch summary for review
leduckhc Aug 11, 2026
40f42a9
fix(docs): add the ocr findings that didn't make the previous commit
leduckhc Aug 11, 2026
749a962
fix(docs): five medium-severity audit findings from ocr review
leduckhc Aug 11, 2026
00fa467
fix(docs): the medium-severity ocr findings worth fixing
leduckhc Aug 11, 2026
072eeff
merge main: SPEC-46 P1 + ports features
leduckhc Aug 11, 2026
5089d52
fix(test): re-sync the shared snapshots fixture after the main merge
leduckhc Aug 11, 2026
fb14296
Merge origin/main (#157) into feat/serving-html
leduckhc Aug 11, 2026
fca3ed8
fix(docs): migrate from snackbar to StatusCenter (SPEC-48)
leduckhc Aug 11, 2026
657a7f1
fix(qa): route doc link copy through StatusCenter, not showSnackBar (…
leduckhc Aug 11, 2026
89c8e48
fix(docs): address CodeRabbit review on SPEC-46 P1
leduckhc Aug 11, 2026
6892bd0
fix(docs): the published URL is plain HTTP on the tailnet IP, not https
leduckhc Aug 11, 2026
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
2 changes: 2 additions & 0 deletions app/lib/app/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../status/activity_screen.dart';
import '../pairing/onboarding_controller.dart';
import '../pairing/onboarding_screen.dart';
import '../pairing/readiness.dart';
import '../ui/docs/docs_screen.dart';
import '../ui/home/closed_screen.dart';
import '../ui/home/home_screen.dart';
import '../ui/ports/ports_screen.dart';
Expand Down Expand Up @@ -63,6 +64,7 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (_, s) =>
PortsScreen(repoId: s.uri.queryParameters['repo']),
),
GoRoute(path: 'docs', builder: (_, _) => const DocsScreen()),
GoRoute(
path: 'diagnostics',
builder: (_, _) => const DiagnosticsScreen(),
Expand Down
4 changes: 4 additions & 0 deletions app/lib/app/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ const kRouteActivity = '$kRouteRepos/activity';
/// optional `?repo=<id>` that pre-selects the *This repo* filter.
const kRoutePorts = '$kRouteRepos/ports';

/// The global Docs screen (SPEC-46 P1) — the repo's mockups and specs, grouped
/// repo → worktree.
const kRouteDocs = '$kRouteRepos/docs';

/// A single session's screen.
String routeForSession(String sessionId) => '$kRouteRepos/session/$sessionId';
117 changes: 108 additions & 9 deletions app/lib/desktop/chat/desktop_sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import 'package:phosphoricons_flutter/phosphoricons_flutter.dart';
import '../../app/theme.dart';
import '../../store/connection.dart';
import '../../store/models.dart';
import '../../store/docs.dart';
import '../../store/ports.dart';
import '../../store/store.dart';
import '../../ui/home/repo_chips.dart';
import '../../ui/docs/docs_popover.dart';
import '../../ui/docs/doc_preview.dart';
import '../../ui/ports/ports_popover.dart';
import '../../ui/ports/session_ports_glyph.dart';
import '../../ui/widgets/pr_state_style.dart';
Expand Down Expand Up @@ -498,6 +501,11 @@ class _WorktreeGroupState extends ConsumerState<_WorktreeGroup> {
// menu back to a diff pill under the cursor while the popover is open. The
// popover reports its open-state through `onOpenChanged` (SPEC-41 §5 trap).
bool _portsOpen = false;
bool _docsOpen = false;

// Hold the ref-counted docs watch while any worktree group is mounted (D11),
// the same discipline as the ports watch below.
late final DocsWatch _docsWatch = ref.read(docsWatchProvider);

// Hold the ref-counted ports watch while any worktree group is mounted in the
// sidebar, so the server polls `lsof` only while the sidebar is up (SPEC-41
Expand All @@ -509,11 +517,13 @@ class _WorktreeGroupState extends ConsumerState<_WorktreeGroup> {
void initState() {
super.initState();
_portsWatch.watch();
_docsWatch.watch();
}

@override
void dispose() {
_portsWatch.release();
_docsWatch.release();
super.dispose();
}

Expand Down Expand Up @@ -620,7 +630,8 @@ class _WorktreeGroupState extends ConsumerState<_WorktreeGroup> {
if (_hovering ||
_focused ||
_menuOpen ||
_portsOpen)
_portsOpen ||
_docsOpen)
_WorktreeMenuButton(
worktree: worktree,
onMenuOpened: () =>
Expand Down Expand Up @@ -705,6 +716,15 @@ class _WorktreeGroupState extends ConsumerState<_WorktreeGroup> {
),
),
),
// Docs glyph, sharing the trailing slot with the
// ports plug (mockup Card 2 right frame). Renders
// nothing when the branch owns no docs.
_SubRowDocsGlyph(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
worktreePath: worktree.path,
branch: branch,
onOpenChanged: (open) =>
setState(() => _docsOpen = open),
),
Comment thread
leduckhc marked this conversation as resolved.
// Ports glyph, right-aligned on the same 8 pt edge
// as line 1's swap (SPEC-41 §5). 14 pt glyph in a
// 22 × 16 target: wider than tall, so the fixed
Expand Down Expand Up @@ -898,12 +918,72 @@ class _WorktreeMenuButton extends ConsumerWidget {
}
}

/// The worktree sub-row's docs glyph (SPEC-46), desktop form. A 14 pt glyph in
/// a fixed 22 × 16 hit target so the sub-row keeps its reserved height. Opens
/// the docs popover; renders nothing when the branch owns no docs.
class _SubRowDocsGlyph extends ConsumerStatefulWidget {
const _SubRowDocsGlyph({
required this.worktreePath,
required this.branch,
required this.onOpenChanged,
});

final String worktreePath;
final String branch;
final ValueChanged<bool> onOpenChanged;

@override
ConsumerState<_SubRowDocsGlyph> createState() => _SubRowDocsGlyphState();
}

class _SubRowDocsGlyphState extends ConsumerState<_SubRowDocsGlyph> {
// The DocsPopover lives inside this subtree. When the branch's last doc goes
// away we return SizedBox.shrink() and the popover unmounts WITHOUT reporting
// closed (its dispose only cancels timers), so the host's `_docsOpen` latch
// would stay stuck and keep the overflow menu replacing the diff pill. We own
// the mount, so we clear the latch on the non-empty->empty edge. Doing it here
// (not in the popover's dispose) avoids a setState on the host during the row
// teardown that unmounts glyph and host together.
bool _hadDocs = false;

@override
Widget build(BuildContext context) {
final docs = ref.watch(docsForWorktreeProvider(widget.worktreePath));
if (docs.isEmpty) {
if (_hadDocs) {
_hadDocs = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) widget.onOpenChanged(false);
});
}
return const SizedBox.shrink();
}
_hadDocs = true;
return SizedBox(
key: ValueKey('docsSubRowTarget-${widget.worktreePath}'),
width: 22,
height: 16,
child: Center(
child: DocsPopover(
branch: widget.branch,
docs: docs,
onOpenChanged: widget.onOpenChanged,
// The preview is a widget in a sheet (D12); Navigator.of works on the
// MaterialApp(home:) desktop shell — unlike `context.go`, which has no
// GoRouter there.
onOpenDoc: (doc) => showDocPreviewSheet(context, doc),
),
),
);
}
}

/// The worktree sub-row's ports glyph (SPEC-41 §5), desktop form. A 14 pt glyph
/// in a fixed 22 × 16 hit target — wider, not taller — so the sub-row keeps its
/// reserved 16 pt height. Hit testing is bounded by this box (Flutter clips to
/// the parent), so the target is honestly 22 × 16, not 22 × 22. Renders nothing
/// when the branch is serving nothing.
class _SubRowPortsGlyph extends ConsumerWidget {
class _SubRowPortsGlyph extends ConsumerStatefulWidget {
const _SubRowPortsGlyph({
required this.worktreePath,
required this.branch,
Expand All @@ -915,22 +995,41 @@ class _SubRowPortsGlyph extends ConsumerWidget {
final ValueChanged<bool> onOpenChanged;

@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(portsGlyphStateProvider(worktreePath));
if (state == PortsGlyphState.none) return const SizedBox.shrink();
final ports = ref.watch(portsForWorktreeProvider(worktreePath));
ConsumerState<_SubRowPortsGlyph> createState() => _SubRowPortsGlyphState();
}

class _SubRowPortsGlyphState extends ConsumerState<_SubRowPortsGlyph> {
// Same latch invariant as _SubRowDocsGlyph: PortsPopover unmounts without
// reporting closed when the branch stops serving, so clear `_portsOpen` on the
// serving->none edge from the side that owns the mount.
bool _wasServing = false;

@override
Widget build(BuildContext context) {
final state = ref.watch(portsGlyphStateProvider(widget.worktreePath));
if (state == PortsGlyphState.none) {
if (_wasServing) {
_wasServing = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) widget.onOpenChanged(false);
});
}
return const SizedBox.shrink();
}
_wasServing = true;
final ports = ref.watch(portsForWorktreeProvider(widget.worktreePath));
return SizedBox(
key: ValueKey('portsSubRowTarget-$worktreePath'),
key: ValueKey('portsSubRowTarget-${widget.worktreePath}'),
width: 22,
height: 16,
child: Center(
child: PortsPopover(
state: state,
count: ports.length,
branch: branch,
branch: widget.branch,
ports: ports,
nowMs: DateTime.now().millisecondsSinceEpoch,
onOpenChanged: onOpenChanged,
onOpenChanged: widget.onOpenChanged,
),
),
);
Expand Down
26 changes: 25 additions & 1 deletion app/lib/store/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class MakitConnState {
this.useFake = false,
this.lastError,
this.pushRegistered = false,
this.serverIsLocal = false,
});

/// Every server this device has paired with. Only [activeServer] holds a live
Expand Down Expand Up @@ -125,6 +126,15 @@ class MakitConnState {
/// (SPEC-07). Resets on reconnect; used by Settings to show wake status.
final bool pushRegistered;

/// SPEC-46 D8 rev 2: whether this client shares a machine with the server,
/// **as stated by the server** in `hello.ack`.
///
/// Never inferred from [PairedServer.host]: mDNS rediscovery rewrites that
/// behind us, and a loopback-looking host is not proof anyway. Governs whether
/// a document can be opened directly (`docs.open`) or has to be published.
/// Defaults false, so the safe path (publish) is the fallback.
final bool serverIsLocal;

bool get paired => activeServer != null || useFake || _wsUrl.isNotEmpty;

MakitConnState copyWith({
Expand All @@ -134,6 +144,7 @@ class MakitConnState {
bool? useFake,
String? lastError,
bool? pushRegistered,
bool? serverIsLocal,
bool clearError = false,
bool clearServer = false,
bool clearPushRegistered = false,
Expand All @@ -146,6 +157,7 @@ class MakitConnState {
pushRegistered: clearPushRegistered
? false
: (pushRegistered ?? this.pushRegistered),
serverIsLocal: serverIsLocal ?? this.serverIsLocal,
);
}

Expand Down Expand Up @@ -510,7 +522,19 @@ class ConnectionController extends StateNotifier<MakitConnState> {

final ws = _transportFactory();
_ws = ws;
_wsSub = ws.frames.listen(_inFrames.add);
// Reset serverIsLocal on each new connection; it will be set from hello.ack if local.
state = state.copyWith(serverIsLocal: false);
_wsSub = ws.frames.listen((env) {
// D8 rev 2: the server states whether we share its machine. Captured here,
// before fan-out, so it is set by the time any screen reads it.
if (env.t == MsgType.helloAck) {
final isLocal = env.body['isLocal'];
// Absent (an older server) is treated as remote: publishing works
// everywhere, so the fallback must be the one that cannot be wrong.
state = state.copyWith(serverIsLocal: isLocal == true);
}
_inFrames.add(env);
});
_wsStateSub = ws.state.listen((s) {
// Clear any stale "unreachable" error once we're actually connected, so
// the connection chip doesn't keep showing a dead-server message after a
Expand Down
Loading
Loading