@task-mcp/shared 1.0.28 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algorithms/index.d.ts +1 -1
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +1 -1
- package/dist/algorithms/index.js.map +1 -1
- package/dist/algorithms/topological-sort.d.ts +21 -1
- package/dist/algorithms/topological-sort.d.ts.map +1 -1
- package/dist/algorithms/topological-sort.js +12 -1
- package/dist/algorithms/topological-sort.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/session.d.ts +521 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +79 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +20 -6
- package/dist/schemas/task.js.map +1 -1
- package/dist/schemas/view.d.ts +18 -18
- package/dist/utils/hierarchy.d.ts.map +1 -1
- package/dist/utils/hierarchy.js +6 -3
- package/dist/utils/hierarchy.js.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +17 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/plan-parser.d.ts +57 -0
- package/dist/utils/plan-parser.d.ts.map +1 -0
- package/dist/utils/plan-parser.js +377 -0
- package/dist/utils/plan-parser.js.map +1 -0
- package/dist/utils/terminal-ui.d.ts +129 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -1
- package/dist/utils/terminal-ui.js +191 -0
- package/dist/utils/terminal-ui.js.map +1 -1
- package/dist/utils/terminal-ui.test.js +236 -0
- package/dist/utils/terminal-ui.test.js.map +1 -1
- package/package.json +2 -2
- package/src/algorithms/index.ts +3 -0
- package/src/algorithms/topological-sort.ts +31 -1
- package/src/schemas/index.ts +11 -0
- package/src/schemas/session.ts +100 -0
- package/src/schemas/task.ts +30 -16
- package/src/utils/hierarchy.ts +8 -3
- package/src/utils/index.ts +31 -0
- package/src/utils/plan-parser.ts +478 -0
- package/src/utils/terminal-ui.test.ts +286 -0
- package/src/utils/terminal-ui.ts +315 -0
|
@@ -829,3 +829,289 @@ describe("BOX constants", () => {
|
|
|
829
829
|
expect(BOX.dblVertical).toBe("║");
|
|
830
830
|
});
|
|
831
831
|
});
|
|
832
|
+
|
|
833
|
+
// =============================================================================
|
|
834
|
+
// Status Symbols Tests
|
|
835
|
+
// =============================================================================
|
|
836
|
+
|
|
837
|
+
import {
|
|
838
|
+
STATUS_SYMBOLS,
|
|
839
|
+
formatStatusSymbol,
|
|
840
|
+
PRIORITY_LEVELS,
|
|
841
|
+
formatPriorityBadge,
|
|
842
|
+
renderSection,
|
|
843
|
+
renderSections,
|
|
844
|
+
renderAlert,
|
|
845
|
+
renderStats,
|
|
846
|
+
formatDueDate,
|
|
847
|
+
formatTaskLine,
|
|
848
|
+
} from "./terminal-ui.js";
|
|
849
|
+
|
|
850
|
+
describe("STATUS_SYMBOLS", () => {
|
|
851
|
+
test("has checkbox-style symbols for all statuses", () => {
|
|
852
|
+
expect(STATUS_SYMBOLS.pending).toBe("[ ]");
|
|
853
|
+
expect(STATUS_SYMBOLS.in_progress).toBe("[~]");
|
|
854
|
+
expect(STATUS_SYMBOLS.completed).toBe("[x]");
|
|
855
|
+
expect(STATUS_SYMBOLS.blocked).toBe("[!]");
|
|
856
|
+
expect(STATUS_SYMBOLS.cancelled).toBe("[-]");
|
|
857
|
+
expect(STATUS_SYMBOLS.deferred).toBe("[.]");
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
describe("formatStatusSymbol", () => {
|
|
862
|
+
test("returns colored symbol for known status", () => {
|
|
863
|
+
const result = formatStatusSymbol("pending");
|
|
864
|
+
expect(stripAnsi(result)).toBe("[ ]");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("returns colored symbol for blocked status", () => {
|
|
868
|
+
const result = formatStatusSymbol("blocked");
|
|
869
|
+
expect(stripAnsi(result)).toBe("[!]");
|
|
870
|
+
expect(result).toContain("\x1b[31m"); // red
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test("returns unknown symbol for invalid status", () => {
|
|
874
|
+
const result = formatStatusSymbol("invalid");
|
|
875
|
+
expect(stripAnsi(result)).toBe("[?]");
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// =============================================================================
|
|
880
|
+
// Priority Badge Tests
|
|
881
|
+
// =============================================================================
|
|
882
|
+
|
|
883
|
+
describe("PRIORITY_LEVELS", () => {
|
|
884
|
+
test("has P0-P3 labels", () => {
|
|
885
|
+
expect(PRIORITY_LEVELS.critical.label).toBe("P0");
|
|
886
|
+
expect(PRIORITY_LEVELS.high.label).toBe("P1");
|
|
887
|
+
expect(PRIORITY_LEVELS.medium.label).toBe("P2");
|
|
888
|
+
expect(PRIORITY_LEVELS.low.label).toBe("P3");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test("critical has background styling", () => {
|
|
892
|
+
expect(PRIORITY_LEVELS.critical.bg).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
describe("formatPriorityBadge", () => {
|
|
897
|
+
test("formats critical with background", () => {
|
|
898
|
+
const result = formatPriorityBadge("critical");
|
|
899
|
+
expect(result).toContain("P0");
|
|
900
|
+
expect(result).toContain("\x1b[41m"); // red background
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test("formats high priority", () => {
|
|
904
|
+
const result = formatPriorityBadge("high");
|
|
905
|
+
expect(stripAnsi(result)).toBe("P1");
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("returns dashes for unknown priority", () => {
|
|
909
|
+
const result = formatPriorityBadge("unknown");
|
|
910
|
+
expect(stripAnsi(result)).toBe("--");
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// =============================================================================
|
|
915
|
+
// Section Renderer Tests
|
|
916
|
+
// =============================================================================
|
|
917
|
+
|
|
918
|
+
describe("renderSection", () => {
|
|
919
|
+
test("renders header and items", () => {
|
|
920
|
+
const items = [
|
|
921
|
+
{ primary: "Task 1" },
|
|
922
|
+
{ primary: "Task 2" },
|
|
923
|
+
];
|
|
924
|
+
const lines = renderSection("Test Section", items);
|
|
925
|
+
|
|
926
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
927
|
+
expect(stripAnsi(lines[0] ?? "")).toContain("Test Section");
|
|
928
|
+
expect(lines.some(l => l.includes("Task 1"))).toBe(true);
|
|
929
|
+
expect(lines.some(l => l.includes("Task 2"))).toBe(true);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test("renders with icon", () => {
|
|
933
|
+
const lines = renderSection("Tasks", [], { icon: "🎯" });
|
|
934
|
+
expect(stripAnsi(lines[0] ?? "")).toContain("🎯");
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test("renders empty state message", () => {
|
|
938
|
+
const lines = renderSection("Empty", [], { emptyMessage: "No items" });
|
|
939
|
+
expect(lines.some(l => stripAnsi(l).includes("No items"))).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("renders item details", () => {
|
|
943
|
+
const items = [
|
|
944
|
+
{ primary: "Task", details: ["Detail 1", "Detail 2"] },
|
|
945
|
+
];
|
|
946
|
+
const lines = renderSection("With Details", items);
|
|
947
|
+
expect(lines.some(l => l.includes("Detail 1"))).toBe(true);
|
|
948
|
+
expect(lines.some(l => l.includes("Detail 2"))).toBe(true);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("renders prefix and suffix", () => {
|
|
952
|
+
const items = [
|
|
953
|
+
{ primary: "Task", prefix: "[x]", suffix: "P1" },
|
|
954
|
+
];
|
|
955
|
+
const lines = renderSection("Test", items);
|
|
956
|
+
expect(lines.some(l => l.includes("[x]"))).toBe(true);
|
|
957
|
+
expect(lines.some(l => l.includes("P1"))).toBe(true);
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
describe("renderSections", () => {
|
|
962
|
+
test("renders multiple sections with spacing", () => {
|
|
963
|
+
const sections = [
|
|
964
|
+
{ title: "Section 1", items: [{ primary: "Item 1" }] },
|
|
965
|
+
{ title: "Section 2", items: [{ primary: "Item 2" }] },
|
|
966
|
+
];
|
|
967
|
+
const lines = renderSections(sections);
|
|
968
|
+
|
|
969
|
+
expect(lines.some(l => stripAnsi(l).includes("Section 1"))).toBe(true);
|
|
970
|
+
expect(lines.some(l => stripAnsi(l).includes("Section 2"))).toBe(true);
|
|
971
|
+
expect(lines.includes("")).toBe(true); // spacing between sections
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// =============================================================================
|
|
976
|
+
// Alert Tests
|
|
977
|
+
// =============================================================================
|
|
978
|
+
|
|
979
|
+
describe("renderAlert", () => {
|
|
980
|
+
test("renders info alert", () => {
|
|
981
|
+
const lines = renderAlert("Test message", { severity: "info" });
|
|
982
|
+
expect(lines.length).toBe(3);
|
|
983
|
+
expect(lines[1]).toContain("Test message");
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("renders warning alert", () => {
|
|
987
|
+
const lines = renderAlert("Warning!", { severity: "warning" });
|
|
988
|
+
expect(lines[1]).toContain("Warning!");
|
|
989
|
+
expect(lines[1]).toContain("⚠");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("renders with custom icon", () => {
|
|
993
|
+
const lines = renderAlert("Custom", { icon: "★" });
|
|
994
|
+
expect(lines[1]).toContain("★");
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// =============================================================================
|
|
999
|
+
// Stats Tests
|
|
1000
|
+
// =============================================================================
|
|
1001
|
+
|
|
1002
|
+
describe("renderStats", () => {
|
|
1003
|
+
test("renders stat items", () => {
|
|
1004
|
+
const stats = [
|
|
1005
|
+
{ label: "Total", value: 10 },
|
|
1006
|
+
{ label: "Done", value: 5 },
|
|
1007
|
+
];
|
|
1008
|
+
const result = renderStats(stats);
|
|
1009
|
+
expect(stripAnsi(result)).toContain("Total:");
|
|
1010
|
+
expect(stripAnsi(result)).toContain("10");
|
|
1011
|
+
expect(stripAnsi(result)).toContain("Done:");
|
|
1012
|
+
expect(stripAnsi(result)).toContain("5");
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test("applies color to values", () => {
|
|
1016
|
+
const stats = [
|
|
1017
|
+
{ label: "Blocked", value: 3, color: "red" as const },
|
|
1018
|
+
];
|
|
1019
|
+
const result = renderStats(stats);
|
|
1020
|
+
expect(result).toContain("\x1b[31m"); // red
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// =============================================================================
|
|
1025
|
+
// Due Date Tests
|
|
1026
|
+
// =============================================================================
|
|
1027
|
+
|
|
1028
|
+
describe("formatDueDate", () => {
|
|
1029
|
+
test("returns dashes for null/undefined", () => {
|
|
1030
|
+
expect(stripAnsi(formatDueDate(null))).toBe("--");
|
|
1031
|
+
expect(stripAnsi(formatDueDate(undefined))).toBe("--");
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test("formats today", () => {
|
|
1035
|
+
const today = new Date().toISOString().split("T")[0];
|
|
1036
|
+
const result = formatDueDate(today);
|
|
1037
|
+
expect(stripAnsi(result)).toBe("today");
|
|
1038
|
+
expect(result).toContain("\x1b[33m"); // yellow
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("formats tomorrow", () => {
|
|
1042
|
+
const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0];
|
|
1043
|
+
const result = formatDueDate(tomorrow);
|
|
1044
|
+
expect(stripAnsi(result)).toBe("tomorrow");
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("formats overdue dates", () => {
|
|
1048
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().split("T")[0];
|
|
1049
|
+
const result = formatDueDate(yesterday);
|
|
1050
|
+
expect(stripAnsi(result)).toContain("overdue");
|
|
1051
|
+
expect(result).toContain("\x1b[31m"); // red
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test("formats dates within a week", () => {
|
|
1055
|
+
const inThreeDays = new Date(Date.now() + 3 * 86400000).toISOString().split("T")[0];
|
|
1056
|
+
const result = formatDueDate(inThreeDays);
|
|
1057
|
+
expect(stripAnsi(result)).toBe("3d");
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// =============================================================================
|
|
1062
|
+
// Task Line Tests
|
|
1063
|
+
// =============================================================================
|
|
1064
|
+
|
|
1065
|
+
describe("formatTaskLine", () => {
|
|
1066
|
+
test("formats basic task", () => {
|
|
1067
|
+
const task = {
|
|
1068
|
+
id: "task_abc123def456",
|
|
1069
|
+
title: "Test task",
|
|
1070
|
+
status: "pending",
|
|
1071
|
+
};
|
|
1072
|
+
const result = formatTaskLine(task);
|
|
1073
|
+
expect(stripAnsi(result)).toContain("[ ]");
|
|
1074
|
+
expect(stripAnsi(result)).toContain("f456"); // last 4 chars of id
|
|
1075
|
+
expect(stripAnsi(result)).toContain("Test task");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test("formats task with priority", () => {
|
|
1079
|
+
const task = {
|
|
1080
|
+
id: "task_123",
|
|
1081
|
+
title: "High priority task",
|
|
1082
|
+
status: "in_progress",
|
|
1083
|
+
priority: "high",
|
|
1084
|
+
};
|
|
1085
|
+
const result = formatTaskLine(task);
|
|
1086
|
+
expect(stripAnsi(result)).toContain("[~]");
|
|
1087
|
+
expect(stripAnsi(result)).toContain("P1");
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("formats task with due date", () => {
|
|
1091
|
+
const today = new Date().toISOString().split("T")[0]!;
|
|
1092
|
+
const task = {
|
|
1093
|
+
id: "task_123",
|
|
1094
|
+
title: "Due today",
|
|
1095
|
+
status: "pending",
|
|
1096
|
+
dueDate: today,
|
|
1097
|
+
};
|
|
1098
|
+
const result = formatTaskLine(task);
|
|
1099
|
+
expect(stripAnsi(result)).toContain("today");
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test("respects options", () => {
|
|
1103
|
+
const task = {
|
|
1104
|
+
id: "task_123456789",
|
|
1105
|
+
title: "Test",
|
|
1106
|
+
status: "pending",
|
|
1107
|
+
priority: "low",
|
|
1108
|
+
};
|
|
1109
|
+
const result = formatTaskLine(task, {
|
|
1110
|
+
showId: false,
|
|
1111
|
+
showPriority: false,
|
|
1112
|
+
showDueDate: false,
|
|
1113
|
+
});
|
|
1114
|
+
expect(stripAnsi(result)).not.toContain("6789");
|
|
1115
|
+
expect(stripAnsi(result)).not.toContain("P3");
|
|
1116
|
+
});
|
|
1117
|
+
});
|
package/src/utils/terminal-ui.ts
CHANGED
|
@@ -783,3 +783,318 @@ export function banner(text: string): string {
|
|
|
783
783
|
|
|
784
784
|
return lines.map((l) => c.cyan(l)).join("\n");
|
|
785
785
|
}
|
|
786
|
+
|
|
787
|
+
// =============================================================================
|
|
788
|
+
// Status Symbols (Checkbox Style)
|
|
789
|
+
// =============================================================================
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Checkbox-style status symbols for task display
|
|
793
|
+
* Designed for quick visual scanning
|
|
794
|
+
*/
|
|
795
|
+
export const STATUS_SYMBOLS = {
|
|
796
|
+
pending: "[ ]",
|
|
797
|
+
in_progress: "[~]",
|
|
798
|
+
completed: "[x]",
|
|
799
|
+
blocked: "[!]",
|
|
800
|
+
cancelled: "[-]",
|
|
801
|
+
deferred: "[.]",
|
|
802
|
+
} as const;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Format status with checkbox symbol and color
|
|
806
|
+
*/
|
|
807
|
+
export function formatStatusSymbol(status: string): string {
|
|
808
|
+
const symbol = STATUS_SYMBOLS[status as keyof typeof STATUS_SYMBOLS] ?? "[?]";
|
|
809
|
+
const colorFn = statusColors[status] ?? c.white;
|
|
810
|
+
return colorFn(symbol);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// =============================================================================
|
|
814
|
+
// Priority Badge (P0-P3 Style)
|
|
815
|
+
// =============================================================================
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Priority levels with P0-P3 notation
|
|
819
|
+
*/
|
|
820
|
+
export const PRIORITY_LEVELS = {
|
|
821
|
+
critical: { label: "P0", color: "brightRed" as const, bg: true },
|
|
822
|
+
high: { label: "P1", color: "red" as const, bg: false },
|
|
823
|
+
medium: { label: "P2", color: "yellow" as const, bg: false },
|
|
824
|
+
low: { label: "P3", color: "gray" as const, bg: false },
|
|
825
|
+
} as const;
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Format priority as P0-P3 badge with appropriate styling
|
|
829
|
+
*/
|
|
830
|
+
export function formatPriorityBadge(priority: string): string {
|
|
831
|
+
const level = PRIORITY_LEVELS[priority as keyof typeof PRIORITY_LEVELS];
|
|
832
|
+
if (!level) return c.gray("--");
|
|
833
|
+
|
|
834
|
+
if (level.bg && priority === "critical") {
|
|
835
|
+
// P0 gets inverse (background) styling for maximum visibility
|
|
836
|
+
return `\x1b[41m\x1b[37m ${level.label} \x1b[0m`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return color(level.label, level.color);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// =============================================================================
|
|
843
|
+
// Section Renderer (GitHub CLI Style)
|
|
844
|
+
// =============================================================================
|
|
845
|
+
|
|
846
|
+
export interface SectionItem {
|
|
847
|
+
/** Primary text (e.g., task title) */
|
|
848
|
+
primary: string;
|
|
849
|
+
/** Secondary text (e.g., status, metadata) */
|
|
850
|
+
secondary?: string;
|
|
851
|
+
/** Detail lines shown below the item */
|
|
852
|
+
details?: string[];
|
|
853
|
+
/** Item prefix (e.g., status symbol) */
|
|
854
|
+
prefix?: string;
|
|
855
|
+
/** Item suffix (e.g., priority badge, due date) */
|
|
856
|
+
suffix?: string;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export interface SectionOptions {
|
|
860
|
+
/** Section icon/emoji */
|
|
861
|
+
icon?: string;
|
|
862
|
+
/** Indent level for items (default: 2) */
|
|
863
|
+
indent?: number;
|
|
864
|
+
/** Show item count in header */
|
|
865
|
+
showCount?: boolean;
|
|
866
|
+
/** Header color */
|
|
867
|
+
headerColor?: ColorName;
|
|
868
|
+
/** Empty state message */
|
|
869
|
+
emptyMessage?: string;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Render a section with header and indented items (GitHub CLI style)
|
|
874
|
+
*
|
|
875
|
+
* Example output:
|
|
876
|
+
* ```
|
|
877
|
+
* 🎯 Next Action
|
|
878
|
+
* [!] a1b2 Implement auth API P0 due:today
|
|
879
|
+
* → Unblocks 3 tasks (critical path)
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
export function renderSection(
|
|
883
|
+
title: string,
|
|
884
|
+
items: SectionItem[],
|
|
885
|
+
options: SectionOptions = {}
|
|
886
|
+
): string[] {
|
|
887
|
+
const {
|
|
888
|
+
icon,
|
|
889
|
+
indent = 2,
|
|
890
|
+
showCount = false,
|
|
891
|
+
headerColor = "cyan",
|
|
892
|
+
emptyMessage = "None",
|
|
893
|
+
} = options;
|
|
894
|
+
|
|
895
|
+
const lines: string[] = [];
|
|
896
|
+
const indentStr = " ".repeat(indent);
|
|
897
|
+
const detailIndent = " ".repeat(indent + 4);
|
|
898
|
+
|
|
899
|
+
// Header
|
|
900
|
+
const countStr = showCount ? c.gray(` (${items.length})`) : "";
|
|
901
|
+
const iconStr = icon ? `${icon} ` : "";
|
|
902
|
+
lines.push(color(`${iconStr}${title}${countStr}`, headerColor));
|
|
903
|
+
|
|
904
|
+
// Empty state
|
|
905
|
+
if (items.length === 0) {
|
|
906
|
+
lines.push(indentStr + c.gray(emptyMessage));
|
|
907
|
+
return lines;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Items
|
|
911
|
+
for (const item of items) {
|
|
912
|
+
const prefix = item.prefix ? `${item.prefix} ` : "";
|
|
913
|
+
const suffix = item.suffix ? ` ${item.suffix}` : "";
|
|
914
|
+
const secondary = item.secondary ? ` ${c.gray(item.secondary)}` : "";
|
|
915
|
+
|
|
916
|
+
lines.push(`${indentStr}${prefix}${item.primary}${secondary}${suffix}`);
|
|
917
|
+
|
|
918
|
+
// Detail lines
|
|
919
|
+
if (item.details && item.details.length > 0) {
|
|
920
|
+
for (const detail of item.details) {
|
|
921
|
+
lines.push(`${detailIndent}${c.gray("→")} ${c.gray(detail)}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return lines;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Render multiple sections with spacing
|
|
931
|
+
*/
|
|
932
|
+
export function renderSections(
|
|
933
|
+
sections: Array<{ title: string; items: SectionItem[]; options?: SectionOptions }>
|
|
934
|
+
): string[] {
|
|
935
|
+
const lines: string[] = [];
|
|
936
|
+
|
|
937
|
+
for (let i = 0; i < sections.length; i++) {
|
|
938
|
+
const section = sections[i];
|
|
939
|
+
if (!section) continue;
|
|
940
|
+
|
|
941
|
+
if (i > 0) {
|
|
942
|
+
lines.push(""); // Add spacing between sections
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
lines.push(...renderSection(section.title, section.items, section.options));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return lines;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// =============================================================================
|
|
952
|
+
// Alert Box
|
|
953
|
+
// =============================================================================
|
|
954
|
+
|
|
955
|
+
export type AlertSeverity = "critical" | "warning" | "info" | "success";
|
|
956
|
+
|
|
957
|
+
export interface AlertOptions {
|
|
958
|
+
severity?: AlertSeverity;
|
|
959
|
+
icon?: string;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const ALERT_STYLES: Record<AlertSeverity, { color: ColorName; icon: string; border: string }> = {
|
|
963
|
+
critical: { color: "red", icon: "✗", border: BOX.dblHorizontal },
|
|
964
|
+
warning: { color: "yellow", icon: "⚠", border: BOX.horizontal },
|
|
965
|
+
info: { color: "cyan", icon: "ℹ", border: BOX.horizontal },
|
|
966
|
+
success: { color: "green", icon: "✓", border: BOX.horizontal },
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Render an alert box with severity styling
|
|
971
|
+
*/
|
|
972
|
+
export function renderAlert(message: string, options: AlertOptions = {}): string[] {
|
|
973
|
+
const { severity = "info", icon } = options;
|
|
974
|
+
const style = ALERT_STYLES[severity];
|
|
975
|
+
const displayIcon = icon ?? style.icon;
|
|
976
|
+
|
|
977
|
+
const content = `${displayIcon} ${message}`;
|
|
978
|
+
const width = displayWidth(content) + 4;
|
|
979
|
+
|
|
980
|
+
const topBorder = color(style.border.repeat(width), style.color);
|
|
981
|
+
const bottomBorder = topBorder;
|
|
982
|
+
|
|
983
|
+
return [topBorder, ` ${color(content, style.color)} `, bottomBorder];
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// =============================================================================
|
|
987
|
+
// Compact Stats Row
|
|
988
|
+
// =============================================================================
|
|
989
|
+
|
|
990
|
+
export interface StatItem {
|
|
991
|
+
label: string;
|
|
992
|
+
value: string | number;
|
|
993
|
+
color?: ColorName;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Render a compact stats row
|
|
998
|
+
* Example: "Total: 24 Done: 8 (33%) Blocked: 4"
|
|
999
|
+
*/
|
|
1000
|
+
export function renderStats(stats: StatItem[], separator = " "): string {
|
|
1001
|
+
return stats
|
|
1002
|
+
.map((stat) => {
|
|
1003
|
+
const valueStr = String(stat.value);
|
|
1004
|
+
const coloredValue = stat.color ? color(valueStr, stat.color) : valueStr;
|
|
1005
|
+
return `${c.gray(stat.label + ":")} ${coloredValue}`;
|
|
1006
|
+
})
|
|
1007
|
+
.join(separator);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// =============================================================================
|
|
1011
|
+
// Due Date Formatter
|
|
1012
|
+
// =============================================================================
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Format due date with relative time and appropriate coloring
|
|
1016
|
+
*/
|
|
1017
|
+
export function formatDueDate(dueDate: string | undefined | null): string {
|
|
1018
|
+
if (!dueDate) return c.gray("--");
|
|
1019
|
+
|
|
1020
|
+
const today = new Date();
|
|
1021
|
+
today.setHours(0, 0, 0, 0);
|
|
1022
|
+
|
|
1023
|
+
const due = new Date(dueDate);
|
|
1024
|
+
due.setHours(0, 0, 0, 0);
|
|
1025
|
+
|
|
1026
|
+
const diffDays = Math.floor((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
1027
|
+
|
|
1028
|
+
if (diffDays < 0) {
|
|
1029
|
+
const overdueDays = Math.abs(diffDays);
|
|
1030
|
+
return c.red(`${overdueDays}d overdue`);
|
|
1031
|
+
} else if (diffDays === 0) {
|
|
1032
|
+
return c.yellow("today");
|
|
1033
|
+
} else if (diffDays === 1) {
|
|
1034
|
+
return c.yellow("tomorrow");
|
|
1035
|
+
} else if (diffDays <= 7) {
|
|
1036
|
+
return c.cyan(`${diffDays}d`);
|
|
1037
|
+
} else {
|
|
1038
|
+
return c.gray(dueDate);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// =============================================================================
|
|
1043
|
+
// Task Line Formatter
|
|
1044
|
+
// =============================================================================
|
|
1045
|
+
|
|
1046
|
+
export interface TaskLineOptions {
|
|
1047
|
+
showId?: boolean;
|
|
1048
|
+
idLength?: number;
|
|
1049
|
+
maxTitleWidth?: number;
|
|
1050
|
+
showPriority?: boolean;
|
|
1051
|
+
showDueDate?: boolean;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Format a single task line for list display
|
|
1056
|
+
* Example: "[~] a1b2 Implement auth API P1 today"
|
|
1057
|
+
*/
|
|
1058
|
+
export function formatTaskLine(
|
|
1059
|
+
task: {
|
|
1060
|
+
id: string;
|
|
1061
|
+
title: string;
|
|
1062
|
+
status: string;
|
|
1063
|
+
priority?: string;
|
|
1064
|
+
dueDate?: string | null;
|
|
1065
|
+
},
|
|
1066
|
+
options: TaskLineOptions = {}
|
|
1067
|
+
): string {
|
|
1068
|
+
const {
|
|
1069
|
+
showId = true,
|
|
1070
|
+
idLength = 4,
|
|
1071
|
+
maxTitleWidth = 40,
|
|
1072
|
+
showPriority = true,
|
|
1073
|
+
showDueDate = true,
|
|
1074
|
+
} = options;
|
|
1075
|
+
|
|
1076
|
+
const parts: string[] = [];
|
|
1077
|
+
|
|
1078
|
+
// Status symbol
|
|
1079
|
+
parts.push(formatStatusSymbol(task.status));
|
|
1080
|
+
|
|
1081
|
+
// Task ID
|
|
1082
|
+
if (showId) {
|
|
1083
|
+
parts.push(c.gray(task.id.slice(-idLength)));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Title (truncated)
|
|
1087
|
+
parts.push(padEnd(truncateStr(task.title, maxTitleWidth), maxTitleWidth));
|
|
1088
|
+
|
|
1089
|
+
// Priority badge
|
|
1090
|
+
if (showPriority && task.priority) {
|
|
1091
|
+
parts.push(formatPriorityBadge(task.priority));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Due date
|
|
1095
|
+
if (showDueDate && task.dueDate) {
|
|
1096
|
+
parts.push(formatDueDate(task.dueDate));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return parts.join(" ");
|
|
1100
|
+
}
|