Grouped list view and ASCII board command (DEW-0012)
- ListCommand: when no --column filter, groups output by column with counts; with --column filter, returns flat list (no redundant column header) - BoardCommand (kanban board): ASCII box per column in config order, self-sizing to longest ticket line; supports --type/--label/--milestone filters registered as 'kanban_board' MCP tool - Test counts updated: 13 MCP tools, 'board' in subcommands list Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
f89b3aa998
commit
27a45e3d52
6 changed files with 121 additions and 5 deletions
92
packages/kanban/lib/src/commands/board_command.dart
Normal file
92
packages/kanban/lib/src/commands/board_command.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import '../kanban_config.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../ticket.dart';
|
||||||
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
class BoardCommand extends DewCommand with DewToolCommand {
|
||||||
|
BoardCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption('type', abbr: 't', help: 'Filter tickets to this type.')
|
||||||
|
..addOption('label', help: 'Filter tickets to this label.')
|
||||||
|
..addOption('milestone', help: 'Filter tickets to this milestone.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'board';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Show ticket counts grouped by column and type.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'kanban_board';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final typeFilter = args['type'] as String?;
|
||||||
|
final labelFilter = args['label'] as String?;
|
||||||
|
final milestoneFilter = args['milestone'] as String?;
|
||||||
|
|
||||||
|
final context = await ProjectContext.find();
|
||||||
|
final config = context.config.kanban;
|
||||||
|
final store = TicketStore(
|
||||||
|
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||||
|
prefix: config.prefix,
|
||||||
|
);
|
||||||
|
|
||||||
|
var tickets = await store.list();
|
||||||
|
if (typeFilter != null) {
|
||||||
|
tickets = tickets.where((t) => t.type == typeFilter).toList();
|
||||||
|
}
|
||||||
|
if (labelFilter != null) {
|
||||||
|
tickets = tickets.where((t) => t.labels.contains(labelFilter)).toList();
|
||||||
|
}
|
||||||
|
if (milestoneFilter != null) {
|
||||||
|
tickets = tickets.where((t) => t.milestones.contains(milestoneFilter)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index by column, preserving config order.
|
||||||
|
final byColumn = <String, List<Ticket>>{};
|
||||||
|
for (final col in config.columns) {
|
||||||
|
byColumn[col.id] = [];
|
||||||
|
}
|
||||||
|
for (final t in tickets) {
|
||||||
|
(byColumn[t.column] ??= []).add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byColumn.values.every((l) => l.isEmpty)) {
|
||||||
|
return 'No tickets found.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each box is at least wide enough for " <longest ticket line> ".
|
||||||
|
final Map<String, List<String>> lines = {};
|
||||||
|
for (final entry in byColumn.entries) {
|
||||||
|
if (entry.value.isEmpty) {
|
||||||
|
lines[entry.key] = [' (empty)'];
|
||||||
|
} else {
|
||||||
|
lines[entry.key] = entry.value
|
||||||
|
.map((t) => ' [${t.id}] (${t.type}) ${t.title}')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final buf = StringBuffer();
|
||||||
|
for (final entry in byColumn.entries) {
|
||||||
|
final col = entry.key;
|
||||||
|
final header = ' $col (${entry.value.length}) ';
|
||||||
|
final colLines = lines[col]!;
|
||||||
|
final contentWidth = [header.length, ...colLines.map((l) => l.length + 2)]
|
||||||
|
.reduce((a, b) => a > b ? a : b);
|
||||||
|
final divider = '─' * contentWidth;
|
||||||
|
buf.writeln('┌$divider┐');
|
||||||
|
buf.writeln('│${header.padRight(contentWidth)}│');
|
||||||
|
buf.writeln('├$divider┤');
|
||||||
|
for (final line in colLines) {
|
||||||
|
buf.writeln('│${line.padRight(contentWidth)}│');
|
||||||
|
}
|
||||||
|
buf.writeln('└$divider┘');
|
||||||
|
}
|
||||||
|
return buf.toString().trimRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../ticket.dart';
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
class ListCommand extends DewCommand with DewToolCommand {
|
class ListCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
@ -50,8 +51,28 @@ class ListCommand extends DewCommand with DewToolCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tickets.isEmpty) return 'No tickets found.';
|
if (tickets.isEmpty) return 'No tickets found.';
|
||||||
return tickets
|
|
||||||
.map((t) => '[${t.id}] (${t.type}) [${t.column}] ${t.title}')
|
// If a column filter is applied, skip the grouping header.
|
||||||
.join('\n');
|
if (columnFilter != null) {
|
||||||
|
return tickets.map((t) => '[${t.id}] (${t.type}) ${t.title}').join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by column and emit with headers.
|
||||||
|
final byColumn = <String, List<Ticket>>{};
|
||||||
|
for (final t in tickets) {
|
||||||
|
(byColumn[t.column] ??= []).add(t);
|
||||||
|
}
|
||||||
|
final buf = StringBuffer();
|
||||||
|
var first = true;
|
||||||
|
for (final column in byColumn.keys) {
|
||||||
|
if (!first) buf.writeln();
|
||||||
|
first = false;
|
||||||
|
final count = byColumn[column]!.length;
|
||||||
|
buf.writeln('$column ($count)');
|
||||||
|
for (final t in byColumn[column]!) {
|
||||||
|
buf.writeln(' [${t.id}] (${t.type}) ${t.title}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.toString().trimRight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
import 'commands/add_comment_command.dart';
|
import 'commands/add_comment_command.dart';
|
||||||
|
import 'commands/board_command.dart';
|
||||||
import 'commands/create_command.dart';
|
import 'commands/create_command.dart';
|
||||||
import 'commands/delete_command.dart';
|
import 'commands/delete_command.dart';
|
||||||
import 'commands/get_command.dart';
|
import 'commands/get_command.dart';
|
||||||
|
|
@ -18,6 +19,7 @@ class KanbanCommand extends DewCommand {
|
||||||
KanbanCommand() {
|
KanbanCommand() {
|
||||||
addSubcommand(CreateCommand());
|
addSubcommand(CreateCommand());
|
||||||
addSubcommand(ListCommand());
|
addSubcommand(ListCommand());
|
||||||
|
addSubcommand(BoardCommand());
|
||||||
addSubcommand(GetCommand());
|
addSubcommand(GetCommand());
|
||||||
addSubcommand(UpdateCommand());
|
addSubcommand(UpdateCommand());
|
||||||
addSubcommand(DeleteCommand());
|
addSubcommand(DeleteCommand());
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ void main() {
|
||||||
expect(
|
expect(
|
||||||
cmd.subcommands.keys,
|
cmd.subcommands.keys,
|
||||||
containsAll([
|
containsAll([
|
||||||
'create', 'list', 'get', 'update', 'delete',
|
'create', 'list', 'board', 'get', 'update', 'delete',
|
||||||
'move', 'search', 'comment', 'config', 'stats', 'link', 'unlink',
|
'move', 'search', 'comment', 'config', 'stats', 'link', 'unlink',
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
@ -36,11 +36,12 @@ void main() {
|
||||||
final registry = CommandRegistry();
|
final registry = CommandRegistry();
|
||||||
registerCommands(registry);
|
registerCommands(registry);
|
||||||
final tools = registry.mcpTools;
|
final tools = registry.mcpTools;
|
||||||
expect(tools, hasLength(12));
|
expect(tools, hasLength(13));
|
||||||
final names = tools.map((t) => t.name).toSet();
|
final names = tools.map((t) => t.name).toSet();
|
||||||
expect(names, {
|
expect(names, {
|
||||||
'kanban_create_ticket',
|
'kanban_create_ticket',
|
||||||
'kanban_list_tickets',
|
'kanban_list_tickets',
|
||||||
|
'kanban_board',
|
||||||
'kanban_get_ticket',
|
'kanban_get_ticket',
|
||||||
'kanban_update_ticket',
|
'kanban_update_ticket',
|
||||||
'kanban_delete_ticket',
|
'kanban_delete_ticket',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue