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:
@@ -96,3 +96,7 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
|
||||
|
||||
Verified done (2026-06-03): the cockpit per-agent permission-mode cycle badge is implemented and wired in claude_meta_sidebar.dart — _PermissionModeBadge (safe cycle default->acceptEdits->plan, Shift-click bypass behind a confirm, live mode from SessionStatus). No further work; closing. The primary-pane equivalent is T-226.', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, '1a0f586aa78d40cb14e96592912176ca', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, '791d993f7f46e80bbb5a4608f55a6038', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:45:56', '2026-06-03 11:45:56', '2026-06-03 11:45:56', NULL, '4f31f40c969af9dc60411414f71489b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-221', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:50:02', '2026-06-03 11:50:02', '2026-06-03 11:50:02', NULL, '141f9d7024902bf3dc1c50337c6aa543', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-218', 'status', 'backlog', 'done', NULL, '2026-06-03 11:50:08', '2026-06-03 11:50:08', '2026-06-03 11:50:08', NULL, '9afcf2b444b85f4c7c5e6ba14d73a04d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'status', 'done', 'done', NULL, '2026-06-03 11:50:23', '2026-06-03 11:50:23', '2026-06-03 11:50:23', NULL, '0abbcea442a884b74a28ff8146dad9d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -231,3 +231,10 @@ Refinement (2026-06-03): Shift+Tab is REJECTED as the trigger — Tab/Shift+Tab
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'task', 'T-164', 'Per-agent permission-mode cycle badge in the cockpit roster', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).
|
||||
|
||||
Verified done (2026-06-03): the cockpit per-agent permission-mode cycle badge is implemented and wired in claude_meta_sidebar.dart — _PermissionModeBadge (safe cycle default->acceptEdits->plan, Shift-click bypass behind a confirm, live mode from SessionStatus). No further work; closing. The primary-pane equivalent is T-226.', 'done', 'low', NULL, NULL, 'D-77', '2026-05-25 09:16:05', '2026-06-03 11:41:17', NULL, '942902ee2825b610673362e5c0add5f7', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-221', 'task', 'T-218', 'Add a clide status one-shot snapshot command', 'Gap 6. Add a `status` command to the dispatcher (currently unknown -> exit 3) returning a single snapshot: active pane, focused file + selection, git summary, layout — the natural first call an agent makes to orient. Acceptance: `clide status` returns a structured snapshot with exit 0. Depends on the pane/editor registries (C1/C2) for its pane and file fields.
|
||||
|
||||
Scope addition (2026-06-03, from T-220): clide status must surface the focused file + selection from BOTH the editor (EditorRegistry.active, already populated) AND the read-only markdown/decisions reader''s current file (D-81 ReaderNav), since viewer files rightly do not live in EditorRegistry. So ''focused file'' = whichever of {active editor buffer, active reader doc} the user is currently looking at. Pane field comes from the T-219 view-pane snapshot; git summary from git.status; layout from the arrangement.', 'done', 'medium', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 11:50:02', NULL, '976aa2a5126f7d0a7dcfa33eb064fd59', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-218', 'epic', 'T-208', 'Make the live workspace observable to the CLI (D-6 parity)', 'Gap 3 from self-analysis.md, verified in code. PaneRegistry (lib/src/panes/registry.dart) is written ONLY by the pane.spawn IPC path, so the built-in TabContribution panes — Claude (builtin/claude/src/extension.dart:42), Files (builtin/files/.../extension.dart:22), Editor (builtin/editor/.../extension.dart:52) — render in the kernel layout tree but never appear in `pane list` (read at pane_commands.dart:121). EditorRegistry is written by editor.open IPC (the Files tree uses it) but the active Claude pane / focused file / selection are not reflected. Result while the UI is in use: `clide pane list` -> {"panes":[]}, `clide editor active` -> {"active":null}. The ''agent sees what the user sees'' half of D-6 is missing.', 'done', 'high', NULL, NULL, 'D-6', '2026-06-02 18:13:52', '2026-06-03 11:50:08', NULL, 'dd286b771087bfc96d6d3d65e76a6b09', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'task', NULL, 'Shift+Tab work-mode cycler on the focused Claude pane', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.
|
||||
|
||||
Refinement (2026-06-03): Shift+Tab is REJECTED as the trigger — Tab/Shift+Tab are real a11y focus-traversal intents (focus.next/focus.previous) since T-204, so hijacking Shift+Tab would break keyboard navigation. Instead: (a) Ctrl/Cmd+M cycles, intercepted at the focused Claude composer (so it targets that pane''s session, no global ''find focused session'' needed); (b) the status-line mode label becomes a FOCUSABLE button — cycles on click and on Enter/Space when focused (a11y-native, Tab reaches it); (c) a ''Claude: Cycle permission mode'' palette command. Safe trio default->acceptEdits->plan->default; bypassPermissions only via explicit confirmed path.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-03 09:37:54', '2026-06-03 11:50:23', NULL, '38722e333acfbd8f5b8491624c587c69', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -22,6 +22,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
composer is focused, a clickable mode badge in the status line, or the
|
||||
"Claude: Cycle permission mode" palette command — steps default → accept-edits
|
||||
→ plan (bypass stays behind the cockpit's confirm). (T-226)
|
||||
- `clide status` — a one-shot orientation snapshot for agents: workspace, git
|
||||
summary, active editor buffer + selection, viewed reader docs, the live panes
|
||||
the user sees, and the layout. Previously an unknown command (exit 3). (T-221)
|
||||
- `clide pane list` now reflects the live GUI tabs the user sees (Claude, Files,
|
||||
Editor, viewers) alongside PTY panes — each with a stable id, slot, title, and
|
||||
active/visible state — by snapshotting the kernel panel layout at request time.
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/// Tests for the `status` command verb (T-221). The snapshot *contents*
|
||||
/// are assembled in main.dart from live kernel state (verified live); here
|
||||
/// we cover the dispatch mechanics: the verb resolves (no longer exit 3),
|
||||
/// returns the assembled map with exit 0, and is recomputed per request.
|
||||
library;
|
||||
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/src/daemon/status_command.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
IpcRequest req() => IpcRequest(id: '1', cmd: 'status', args: const {});
|
||||
|
||||
test('status resolves and returns the assembled snapshot with exit 0', () async {
|
||||
final d = DaemonDispatcher();
|
||||
registerStatusCommand(
|
||||
d,
|
||||
() async => {
|
||||
'workspace': '/repo',
|
||||
'focusedFile': null,
|
||||
'panes': const [],
|
||||
});
|
||||
final r = await d.dispatch(req());
|
||||
expect(r.ok, isTrue);
|
||||
expect(r.data['workspace'], '/repo');
|
||||
expect(r.data['panes'], isEmpty);
|
||||
});
|
||||
|
||||
test('the snapshot is recomputed on every call (live, not cached)', () async {
|
||||
var n = 0;
|
||||
final d = DaemonDispatcher();
|
||||
registerStatusCommand(d, () async => {'n': ++n});
|
||||
expect((await d.dispatch(req())).data['n'], 1);
|
||||
expect((await d.dispatch(req())).data['n'], 2);
|
||||
});
|
||||
|
||||
test('without registration, status is an unknown command (exit 3 / notFound)', () async {
|
||||
final d = DaemonDispatcher();
|
||||
final r = await d.dispatch(req());
|
||||
expect(r.ok, isFalse);
|
||||
expect(r.error?.kind, IpcErrorKind.notFound);
|
||||
});
|
||||
}
|
||||
@@ -143,4 +143,14 @@ void main() {
|
||||
final c = reg.navFor('builtin.decisions', dataKey: 'id');
|
||||
expect(identical(a, c), isFalse);
|
||||
});
|
||||
|
||||
test('currentByReader maps each reader to its viewed doc; omits empty (T-221)', () {
|
||||
final reg = ReaderNavRegistry(bus);
|
||||
addTearDown(reg.dispose);
|
||||
final md = reg.navFor('builtin.markdown', dataKey: 'path');
|
||||
reg.navFor('builtin.decisions', dataKey: 'id'); // created but nothing viewed
|
||||
expect(reg.currentByReader, isEmpty);
|
||||
md.open('docs/initial-plan.md');
|
||||
expect(reg.currentByReader, {'builtin.markdown': 'docs/initial-plan.md'});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user