tuiboard 0.8.3 → 0.9.0

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.
@@ -1,181 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- classifyStatus,
4
- cwdFromSlug,
5
- cwdShort,
6
- formatAge,
7
- parseTranscript,
8
- type LivePidRecord,
9
- } from "./agents";
10
-
11
- describe("cwdFromSlug", () => {
12
- it("decodes a Windows drive-letter slug", () => {
13
- expect(cwdFromSlug("C--Users-nazza-Documents-Repos-Blits")).toBe(
14
- "C:\\Users\\nazza\\Documents\\Repos\\Blits",
15
- );
16
- });
17
-
18
- it("decodes a POSIX absolute path slug", () => {
19
- expect(cwdFromSlug("-home-foo-projects-myrepo")).toBe(
20
- "/home/foo/projects/myrepo",
21
- );
22
- });
23
-
24
- it("decodes a macOS user directory slug", () => {
25
- expect(cwdFromSlug("-Users-foo-code-blits")).toBe("/Users/foo/code/blits");
26
- });
27
-
28
- it("falls back to host separator for ambiguous bare slugs", () => {
29
- // No leading dash and no drive letter — host OS picks the separator.
30
- const sep = process.platform === "win32" ? "\\" : "/";
31
- expect(cwdFromSlug("workdir-x")).toBe(`workdir${sep}x`);
32
- });
33
- });
34
-
35
- describe("cwdShort", () => {
36
- it("returns the last 3 parts prefixed with ellipsis when path is long", () => {
37
- expect(cwdShort("C:\\Users\\nazza\\Documents\\Repos\\Blits")).toBe(
38
- "…Documents\\Repos\\Blits",
39
- );
40
- });
41
-
42
- it("returns the full path when 3 or fewer parts", () => {
43
- expect(cwdShort("C:\\Users\\nazza")).toBe("C:\\Users\\nazza");
44
- });
45
- });
46
-
47
- describe("classifyStatus", () => {
48
- const now = 1_700_000_000_000; // fixed instant
49
- const minutes = (n: number) => n * 60_000;
50
- const days = (n: number) => n * 86_400_000;
51
-
52
- it("returns live-busy when PID record fresh AND status busy", () => {
53
- const live: LivePidRecord = { mtimeMs: now - minutes(1), status: "busy" };
54
- expect(classifyStatus(now, now, live)).toBe("live-busy");
55
- });
56
-
57
- it("returns live-idle when PID record fresh AND status idle/missing", () => {
58
- const live: LivePidRecord = { mtimeMs: now - minutes(1) };
59
- expect(classifyStatus(now, now, live)).toBe("live-idle");
60
- });
61
-
62
- it("returns stale-pid when PID record older than 5min", () => {
63
- const live: LivePidRecord = { mtimeMs: now - minutes(10), status: "busy" };
64
- expect(classifyStatus(now, now, live)).toBe("stale-pid");
65
- });
66
-
67
- it("returns dormant when no PID and jsonl mtime within 7 days", () => {
68
- expect(classifyStatus(now, now - days(2), undefined)).toBe("dormant");
69
- });
70
-
71
- it("returns archived when jsonl mtime older than 7 days and no PID", () => {
72
- expect(classifyStatus(now, now - days(10), undefined)).toBe("archived");
73
- });
74
- });
75
-
76
- describe("formatAge", () => {
77
- const now = 1_700_000_000_000;
78
-
79
- it("formats seconds", () => {
80
- expect(formatAge(now - 30_000, now)).toBe("30s");
81
- });
82
-
83
- it("formats minutes", () => {
84
- expect(formatAge(now - 5 * 60_000, now)).toBe("5m");
85
- });
86
-
87
- it("formats hours", () => {
88
- expect(formatAge(now - 3 * 3_600_000, now)).toBe("3h");
89
- });
90
-
91
- it("formats days", () => {
92
- expect(formatAge(now - 2 * 86_400_000, now)).toBe("2d");
93
- });
94
-
95
- it("returns dash for zero", () => {
96
- expect(formatAge(0, now)).toBe("—");
97
- });
98
- });
99
-
100
- describe("parseTranscript", () => {
101
- const SAMPLE_JSONL = [
102
- JSON.stringify({ type: "user", message: { role: "user", content: "Ciao" }, gitBranch: "main" }),
103
- JSON.stringify({
104
- type: "assistant",
105
- message: {
106
- role: "assistant",
107
- content: [
108
- { type: "text", text: "Hello" },
109
- { type: "tool_use", name: "Read" },
110
- ],
111
- },
112
- }),
113
- JSON.stringify({ type: "custom-title", customTitle: "Refactor store" }),
114
- ].join("\n");
115
-
116
- it("extracts title, last messages, counts, branch", () => {
117
- const result = parseTranscript(SAMPLE_JSONL);
118
- expect(result.customTitle).toBe("Refactor store");
119
- expect(result.lastUser).toBe("Ciao");
120
- expect(result.firstHumanUser).toBe("Ciao");
121
- expect(result.lastAssistant).toBe("Hello");
122
- expect(result.messageCount).toBe(2);
123
- expect(result.toolCount).toBe(1);
124
- expect(result.gitBranch).toBe("main");
125
- });
126
-
127
- it("tolerates malformed lines", () => {
128
- const broken = SAMPLE_JSONL + "\n{this is not json\n";
129
- const result = parseTranscript(broken);
130
- expect(result.lastUser).toBe("Ciao"); // still got the good lines
131
- });
132
-
133
- it("handles empty input", () => {
134
- const result = parseTranscript("");
135
- expect(result.messageCount).toBe(0);
136
- expect(result.customTitle).toBeUndefined();
137
- });
138
-
139
- it("skips skill-bootstrap and system-tag user messages when picking firstHumanUser", () => {
140
- const lines = [
141
- // Synthetic skill loader injected by Claude Code on /morning
142
- JSON.stringify({
143
- type: "user",
144
- message: {
145
- role: "user",
146
- content: "Base directory for this skill: C:\\Users\\nazza\\.claude\\skills\\morning",
147
- },
148
- }),
149
- // System-injected reminder tag
150
- JSON.stringify({
151
- type: "user",
152
- message: { role: "user", content: "<system-reminder>do the thing</system-reminder>" },
153
- }),
154
- // Real human prompt
155
- JSON.stringify({
156
- type: "user",
157
- message: { role: "user", content: "Davvero buongiorno, partiamo dal recap di ieri" },
158
- }),
159
- ].join("\n");
160
- const result = parseTranscript(lines);
161
- expect(result.firstHumanUser).toBe("Davvero buongiorno, partiamo dal recap di ieri");
162
- // lastUser still tracks the literal last message regardless
163
- expect(result.lastUser).toBe("Davvero buongiorno, partiamo dal recap di ieri");
164
- });
165
-
166
- it("returns undefined firstHumanUser when every user message is synthetic", () => {
167
- const lines = [
168
- JSON.stringify({
169
- type: "user",
170
- message: { role: "user", content: "Base directory for this skill: X" },
171
- }),
172
- JSON.stringify({
173
- type: "user",
174
- message: { role: "user", content: "<task-notification>noisy</task-notification>" },
175
- }),
176
- ].join("\n");
177
- const result = parseTranscript(lines);
178
- expect(result.firstHumanUser).toBeUndefined();
179
- expect(result.lastUser).toBe("<task-notification>noisy</task-notification>");
180
- });
181
- });
@@ -1,224 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import type { Config } from "~/config/loader";
4
- import { createTuiStore, isoAddDays, isoToday } from "./index";
5
-
6
- describe("test runner smoke", () => {
7
- it("can run a trivial assertion", () => {
8
- expect(1 + 1).toBe(2);
9
- });
10
- });
11
-
12
- /** Builds a minimal config with no boards — enough to exercise pure UI actions. */
13
- function emptyConfig(overrides: Partial<Config> = {}): Config {
14
- return {
15
- root: process.cwd(),
16
- loaded: false,
17
- boards: [],
18
- assignees: [],
19
- doneColumn: "Done",
20
- archiveColumn: "Archive",
21
- zones: { planner: "on", agenda: "on", agents: "on" },
22
- ...overrides,
23
- };
24
- }
25
-
26
- describe("UI activeZone", () => {
27
- it("defaults to 'board' on a fresh store", () => {
28
- const store = createTuiStore({ config: emptyConfig() });
29
- expect(store.state.ui.activeZone).toBe("board");
30
- });
31
-
32
- it("setActiveZone updates the zone", () => {
33
- const store = createTuiStore({ config: emptyConfig() });
34
- store.setActiveZone("planner");
35
- expect(store.state.ui.activeZone).toBe("planner");
36
- store.setActiveZone("timeline");
37
- expect(store.state.ui.activeZone).toBe("timeline");
38
- });
39
-
40
- it("setActiveZone('planner') resets row to 0", () => {
41
- const store = createTuiStore({ config: emptyConfig() });
42
- store.setCursor(0, 7);
43
- store.setActiveZone("planner");
44
- expect(store.state.ui.row).toBe(0);
45
- });
46
- });
47
-
48
- describe("UI visibleZones", () => {
49
- it("defaults to all four zones visible", () => {
50
- const store = createTuiStore({ config: emptyConfig() });
51
- expect(store.state.ui.visibleZones).toEqual({
52
- planner: true,
53
- board: true,
54
- timeline: true,
55
- agents: true,
56
- });
57
- });
58
-
59
- it("setZoneVisible flips one zone without touching others", () => {
60
- const store = createTuiStore({ config: emptyConfig() });
61
- store.setZoneVisible("timeline", false);
62
- expect(store.state.ui.visibleZones.timeline).toBe(false);
63
- expect(store.state.ui.visibleZones.planner).toBe(true);
64
- expect(store.state.ui.visibleZones.board).toBe(true);
65
- expect(store.state.ui.visibleZones.agents).toBe(true);
66
- });
67
-
68
- it("setZoneVisible('board', false) is ignored — board is load-bearing", () => {
69
- const store = createTuiStore({ config: emptyConfig() });
70
- store.setZoneVisible("board", false);
71
- expect(store.state.ui.visibleZones.board).toBe(true);
72
- });
73
-
74
- it("hiding the active zone moves activeZone to 'board'", () => {
75
- const store = createTuiStore({ config: emptyConfig() });
76
- store.setActiveZone("timeline");
77
- store.setZoneVisible("timeline", false);
78
- expect(store.state.ui.activeZone).toBe("board");
79
- });
80
- });
81
-
82
- describe("UI cycleActiveZone", () => {
83
- it("cycles through all visible zones in fixed order", () => {
84
- const store = createTuiStore({ config: emptyConfig() });
85
- store.setActiveZone("planner");
86
- store.cycleActiveZone();
87
- expect(store.state.ui.activeZone).toBe("board");
88
- store.cycleActiveZone();
89
- expect(store.state.ui.activeZone).toBe("timeline");
90
- store.cycleActiveZone();
91
- expect(store.state.ui.activeZone).toBe("agents");
92
- store.cycleActiveZone();
93
- expect(store.state.ui.activeZone).toBe("planner"); // wrap
94
- });
95
-
96
- it("skips hidden zones", () => {
97
- const store = createTuiStore({ config: emptyConfig() });
98
- store.setZoneVisible("timeline", false);
99
- store.setActiveZone("board");
100
- store.cycleActiveZone();
101
- expect(store.state.ui.activeZone).toBe("agents"); // timeline skipped
102
- });
103
-
104
- it("is a no-op when only one zone is visible (board only)", () => {
105
- const store = createTuiStore({ config: emptyConfig() });
106
- store.setZoneVisible("planner", false);
107
- store.setZoneVisible("timeline", false);
108
- store.setZoneVisible("agents", false);
109
- store.cycleActiveZone();
110
- expect(store.state.ui.activeZone).toBe("board");
111
- });
112
- });
113
-
114
- describe("isoAddDays", () => {
115
- it("adds and subtracts days", () => {
116
- expect(isoAddDays("2026-05-30", 1)).toBe("2026-05-31");
117
- expect(isoAddDays("2026-05-30", -1)).toBe("2026-05-29");
118
- expect(isoAddDays("2026-05-30", 0)).toBe("2026-05-30");
119
- });
120
-
121
- it("rolls over month and year boundaries", () => {
122
- expect(isoAddDays("2026-05-31", 1)).toBe("2026-06-01");
123
- expect(isoAddDays("2026-12-31", 1)).toBe("2027-01-01");
124
- expect(isoAddDays("2026-03-01", -1)).toBe("2026-02-28");
125
- });
126
- });
127
-
128
- describe("Agenda day navigation", () => {
129
- it("defaults to today (offset 0)", () => {
130
- const store = createTuiStore({ config: emptyConfig() });
131
- expect(store.state.ui.agendaOffset).toBe(0);
132
- expect(store.agendaDate()).toBe(isoToday());
133
- });
134
-
135
- it("shifts the viewed day and reflects it in agendaDate()", () => {
136
- const store = createTuiStore({ config: emptyConfig() });
137
- store.shiftAgendaDay(1);
138
- expect(store.state.ui.agendaOffset).toBe(1);
139
- expect(store.agendaDate()).toBe(isoAddDays(isoToday(), 1));
140
- store.shiftAgendaDay(-3);
141
- expect(store.state.ui.agendaOffset).toBe(-2);
142
- });
143
-
144
- it("resets to today and clamps to ±365", () => {
145
- const store = createTuiStore({ config: emptyConfig() });
146
- store.shiftAgendaDay(1000);
147
- expect(store.state.ui.agendaOffset).toBe(365);
148
- store.resetAgendaDay();
149
- expect(store.state.ui.agendaOffset).toBe(0);
150
- store.shiftAgendaDay(-1000);
151
- expect(store.state.ui.agendaOffset).toBe(-365);
152
- });
153
-
154
- it("resets the timeline cursor when changing day", () => {
155
- const store = createTuiStore({ config: emptyConfig() });
156
- store.setCursor(0, 5);
157
- store.shiftAgendaDay(1);
158
- expect(store.state.ui.row).toBe(0);
159
- });
160
- });
161
-
162
- describe("zones config", () => {
163
- it("enables and shows all zones by default", () => {
164
- const s = createTuiStore({ config: emptyConfig() });
165
- expect(s.state.ui.enabledZones).toEqual({ board: true, planner: true, timeline: true, agents: true });
166
- expect(s.state.ui.visibleZones).toEqual({ board: true, planner: true, timeline: true, agents: true });
167
- });
168
-
169
- it("disables a zone set to off (never enabled, never visible)", () => {
170
- const s = createTuiStore({
171
- config: emptyConfig({ zones: { planner: "on", agenda: "off", agents: "off" } }),
172
- });
173
- expect(s.state.ui.enabledZones.timeline).toBe(false);
174
- expect(s.state.ui.enabledZones.agents).toBe(false);
175
- expect(s.state.ui.visibleZones.timeline).toBe(false);
176
- expect(s.state.ui.visibleZones.agents).toBe(false);
177
- });
178
-
179
- it("a hidden zone is enabled but not visible at start; F-key reveals it", () => {
180
- const s = createTuiStore({
181
- config: emptyConfig({ zones: { planner: "hidden", agenda: "on", agents: "on" } }),
182
- });
183
- expect(s.state.ui.enabledZones.planner).toBe(true);
184
- expect(s.state.ui.visibleZones.planner).toBe(false);
185
- s.toggleZoneDesired("planner");
186
- expect(s.state.ui.visibleZones.planner).toBe(true);
187
- });
188
-
189
- it("F-key toggle is a no-op on a disabled zone", () => {
190
- const s = createTuiStore({
191
- config: emptyConfig({ zones: { planner: "on", agenda: "off", agents: "on" } }),
192
- });
193
- s.toggleZoneDesired("timeline");
194
- expect(s.state.ui.visibleZones.timeline).toBe(false);
195
- });
196
-
197
- it("Shift-Tab cycle skips disabled zones", () => {
198
- const s = createTuiStore({
199
- config: emptyConfig({ zones: { planner: "on", agenda: "off", agents: "off" } }),
200
- });
201
- s.setActiveZone("board");
202
- s.cycleActiveZone();
203
- expect(s.state.ui.activeZone).toBe("planner");
204
- s.cycleActiveZone();
205
- expect(s.state.ui.activeZone).toBe("board"); // timeline + agents skipped
206
- });
207
-
208
- it("responsive fits never force-show a disabled zone", () => {
209
- const s = createTuiStore({
210
- config: emptyConfig({ zones: { planner: "on", agenda: "off", agents: "on" } }),
211
- });
212
- s.applyResponsiveFits({ planner: true, timeline: true, agents: true });
213
- expect(s.state.ui.visibleZones.timeline).toBe(false); // disabled stays off
214
- expect(s.state.ui.visibleZones.agents).toBe(true);
215
- });
216
-
217
- it("responsive hides a zone that doesn't fit; it returns when it fits again", () => {
218
- const s = createTuiStore({ config: emptyConfig() });
219
- s.applyResponsiveFits({ planner: true, timeline: false, agents: true });
220
- expect(s.state.ui.visibleZones.timeline).toBe(false);
221
- s.applyResponsiveFits({ planner: true, timeline: true, agents: true });
222
- expect(s.state.ui.visibleZones.timeline).toBe(true);
223
- });
224
- });
@@ -1,64 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { isoToday, isoTomorrow } from "./index";
4
- import { parseDateShortcut, parseQuickAdd, parseTimeBlockShortcut } from "./parsers";
5
-
6
- describe("parseDateShortcut", () => {
7
- it("maps t / today / oggi to today", () => {
8
- expect(parseDateShortcut("t")).toBe(isoToday());
9
- expect(parseDateShortcut("today")).toBe(isoToday());
10
- expect(parseDateShortcut("oggi")).toBe(isoToday());
11
- });
12
-
13
- it("maps m to tomorrow (consistent with the board's m key)", () => {
14
- expect(parseDateShortcut("m")).toBe(isoTomorrow());
15
- });
16
-
17
- it("keeps tm / tom / tomorrow / domani as tomorrow aliases", () => {
18
- expect(parseDateShortcut("tm")).toBe(isoTomorrow());
19
- expect(parseDateShortcut("tom")).toBe(isoTomorrow());
20
- expect(parseDateShortcut("tomorrow")).toBe(isoTomorrow());
21
- expect(parseDateShortcut("domani")).toBe(isoTomorrow());
22
- });
23
-
24
- it("is case-insensitive", () => {
25
- expect(parseDateShortcut("M")).toBe(isoTomorrow());
26
- expect(parseDateShortcut("T")).toBe(isoToday());
27
- });
28
-
29
- it("clears on empty / dash, fails on garbage", () => {
30
- expect(parseDateShortcut("")).toBeUndefined();
31
- expect(parseDateShortcut("-")).toBeUndefined();
32
- expect(parseDateShortcut("zzz")).toBeNull();
33
- });
34
-
35
- it("parses ISO dates literally", () => {
36
- expect(parseDateShortcut("2026-06-10")).toBe("2026-06-10");
37
- });
38
- });
39
-
40
- describe("parseQuickAdd date tokens", () => {
41
- it("treats a standalone m as tomorrow and strips it from the title", () => {
42
- const r = parseQuickAdd("Pay invoice m");
43
- expect(r.scheduled).toBe(isoTomorrow());
44
- expect(r.title).toBe("Pay invoice");
45
- });
46
-
47
- it("treats a standalone t as today", () => {
48
- const r = parseQuickAdd("Standup t");
49
- expect(r.scheduled).toBe(isoToday());
50
- expect(r.title).toBe("Standup");
51
- });
52
- });
53
-
54
- describe("parseTimeBlockShortcut", () => {
55
- it("parses loose H-H ranges and HH:MM-HH:MM", () => {
56
- expect(parseTimeBlockShortcut("9-11")).toEqual({ startMin: 540, endMin: 660 });
57
- expect(parseTimeBlockShortcut("09:30-10:45")).toEqual({ startMin: 570, endMin: 645 });
58
- });
59
-
60
- it("clears on empty / dash", () => {
61
- expect(parseTimeBlockShortcut("")).toBeUndefined();
62
- expect(parseTimeBlockShortcut("-")).toBeUndefined();
63
- });
64
- });