add clide status orientation snapshot; close Epic C (T-221)

clide status returns a one-shot snapshot for an orienting agent: the
workspace root, a git summary (branch/ahead/behind/clean/counts), the
active editor buffer + selection, the read-only readers' viewed docs
(new ReaderNavRegistry.currentByReader, the T-220 fold), focusedFile,
the live view-pane list (T-219), and the layout (slots + visibility +
focus mode). Previously 'status' was an unknown command (exit 3).

The verb handler (status_command.dart) is a thin Flutter-free wrapper;
the snapshot is assembled in main.dart where the live kernel + subsystem
state is in scope, with readerNav captured post-boot. Composite shape is
verified live; the pieces are unit-tested.

Closes T-221 and T-218 (Epic C) under T-208 'Give Claude hands' --
the observe half of D-6 parity is now in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 13:51:15 +02:00
co-authored by Claude Opus 4.8
parent 6c4c48bfc2
commit 8682564903
8 changed files with 160 additions and 0 deletions
+9
View File
@@ -127,6 +127,15 @@ class ReaderNavRegistry {
final MessageBus _messages;
final Map<String, ReaderNav> _navs = {};
/// The currently-viewed doc for every reader that has one, keyed by
/// publisher id (e.g. `builtin.markdown`, `builtin.decisions`). Used by
/// `clide status` to surface what the user is reading (T-221, D-6 parity) —
/// viewer files aren't editor buffers, so they don't live in EditorRegistry.
Map<String, String> get currentByReader => {
for (final e in _navs.entries)
if (e.value.current != null) e.key: e.value.current!,
};
/// The retained [ReaderNav] for [publisherId], created on first use.
/// [dataKey] is the bus-payload key for this reader's entry.
ReaderNav navFor(String publisherId, {required String dataKey}) {
+63
View File
@@ -36,6 +36,7 @@ import 'package:clide/src/daemon/editor_commands.dart';
import 'package:clide/src/daemon/files_commands.dart';
import 'package:clide/src/daemon/git_commands.dart';
import 'package:clide/src/daemon/pane_commands.dart';
import 'package:clide/src/daemon/status_command.dart';
import 'package:clide/src/daemon/panel_commands.dart';
import 'package:clide/src/daemon/panel_resizer_kernel.dart';
import 'package:clide/src/daemon/pql_commands.dart';
@@ -88,6 +89,9 @@ Future<void> main() async {
DaemonBus? daemonBus;
LayoutArrangement? kernelArrangement;
PanelRegistry? kernelPanels;
// Captured after boot so `clide status` can report the read-only reader's
// viewed doc (D-81), which isn't an editor buffer (T-221).
ReaderNavRegistry? kernelReaderNav;
// IPC socket server (T-99 / T-124, per D-70/71/72). One server per
// workspace; restarted when the active project switches because the
// socket path is workspace-derived. The local DaemonClient connects
@@ -203,6 +207,61 @@ Future<void> main() async {
final pql = PqlClient(workDir: workRoot, toolchain: tc);
registerPqlCommands(dispatcher, pql);
registerPanelCommands(dispatcher, ArrangementPanelResizer(arrangement));
// `clide status` — one-shot orientation snapshot (T-221): active pane,
// focused file + selection, git summary, layout. Assembled here where the
// live kernel + subsystem state is in scope; the reader's viewed doc is
// read from the post-boot-captured ReaderNavRegistry (D-81).
registerStatusCommand(dispatcher, () async {
Map<String, Object?>? gitJson;
try {
final git = await gitClient.status();
gitJson = {
'branch': git.branch,
if (git.upstream != null) 'upstream': git.upstream,
'ahead': git.ahead,
'behind': git.behind,
'clean': git.isClean,
'hasConflicts': git.hasConflicts,
'counts': {
'staged': git.staged.length,
'unstaged': git.unstaged.length,
'untracked': git.untracked.length,
'conflicted': git.conflicted.length,
},
};
} catch (_) {
gitJson = null; // never sink the snapshot on a git hiccup
}
final editorActive = editorRegistry.active;
return {
'workspace': workRoot.path,
'git': gitJson,
'editor': editorActive == null
? null
: {
'id': editorActive.id,
'path': editorActive.path,
'selection': editorActive.selection.toJson(),
'dirty': editorActive.dirty,
},
'readers': kernelReaderNav?.currentByReader ?? const <String, String>{},
'focusedFile': editorActive?.path,
'panes': [for (final v in snapshotViewPanes(panels, arrangement)) v.toJson()],
'layout': {
'focusMode': arrangement.focusModeSlot?.value,
'slots': [
for (final id in arrangement.slotsInOrder)
{
'id': id.value,
'position': arrangement.positionOf(id)?.name,
'visible': arrangement.isVisible(id),
'collapsed': arrangement.isCollapsed(id),
if (arrangement.sizeOf(id) != null) 'size': arrangement.sizeOf(id),
},
],
},
};
});
registerArgvUnwrap(dispatcher);
return dispatcher;
}
@@ -254,6 +313,10 @@ Future<void> main() async {
await swapIpcServer(dispatcher, Directory(path));
},
);
// Expose the reader nav to the `clide status` snapshot (T-221). Boot
// creates it before the daemonClientFactory runs, but the status closure
// only reads it at request time (post-boot), so capturing it here is safe.
kernelReaderNav = services.readerNav;
// Register every built-in. Tier 0 activates only the four that do
// real work; the rest compile in as stubs so the extensions-ui can
+21
View File
@@ -0,0 +1,21 @@
/// Registers the `status` command — a one-shot orientation snapshot
/// (T-221, Gap 6 of self-analysis.md). It is the natural first call an
/// agent makes: active pane, focused file + selection, git summary, and
/// layout, in one round-trip, with exit 0.
///
/// The handler is intentionally thin: the snapshot is assembled by the
/// caller (main.dart), which holds the live kernel + subsystem state
/// (PanelRegistry, LayoutArrangement, EditorRegistry, GitClient,
/// ReaderNavRegistry). This file just exposes the verb and wraps the
/// assembled map, so it stays Flutter-free and trivially testable.
library;
import '../ipc/envelope.dart';
import 'dispatcher.dart';
/// Builds the orientation snapshot at request time. Returns a JSON-able map.
typedef StatusSnapshot = Future<Map<String, Object?>> Function();
void registerStatusCommand(DaemonDispatcher d, StatusSnapshot snapshot) {
d.register('status', (req) async => IpcResponse.ok(id: req.id, data: await snapshot()));
}