tuiboard 0.6.0 → 0.6.1
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/README.md +4 -3
- package/package.json +1 -1
- package/src/input/handleKey.ts +40 -20
- package/src/store/calendar.ts +29 -12
- package/src/store/index.ts +17 -0
- package/src/ui/AgentRow.tsx +5 -3
- package/src/ui/Chrome.tsx +1 -1
- package/src/ui/Modal.tsx +2 -1
- package/src/ui/TimelineView.tsx +6 -2
package/README.md
CHANGED
|
@@ -269,12 +269,13 @@ session (until the next terminal resize).
|
|
|
269
269
|
| `Shift-Tab` | Cycle active zone (virtual → board → timeline → agents) |
|
|
270
270
|
| `F1` / `F2` / `F3` | Toggle visibility of Virtual / Timeline / Agents zones |
|
|
271
271
|
| `z` | Zoom active zone to full screen |
|
|
272
|
+
| `r` | Refresh everything — reload boards from disk, rescan agents, force-refetch the agenda calendar (bypasses the 30-min cache) |
|
|
272
273
|
|
|
273
274
|
### Agenda (timeline zone)
|
|
274
275
|
|
|
275
276
|
| Key | Action |
|
|
276
277
|
|---|---|
|
|
277
|
-
| `[` / `]` | Previous / next day — shows that day's tasks **and** calendar events |
|
|
278
|
+
| `[` / `]` | Previous / next day — shows that day's tasks **and** calendar events (works from any zone) |
|
|
278
279
|
| `\` | Jump back to today |
|
|
279
280
|
| `c` | Arm mode: click a task, then click a slot to schedule (works from any zone) |
|
|
280
281
|
| `j` / `k` | While armed: nudge the block ±15 min |
|
|
@@ -319,8 +320,8 @@ session (until the next terminal resize).
|
|
|
319
320
|
## Status
|
|
320
321
|
|
|
321
322
|
- **v0.6** — adds the Agenda calendar overlay (Google + Microsoft 365,
|
|
322
|
-
read-only, BYO credentials)
|
|
323
|
-
|
|
323
|
+
read-only, BYO credentials), day-navigation (`[` / `]` / `\`) to page tasks
|
|
324
|
+
and events across days, and a manual full-refresh key (`r`).
|
|
324
325
|
- **v0.5** — daily-driver ready. Kanban + virtual + timeline + agents
|
|
325
326
|
all functional, multi-select, undo, atomic file roundtrip, mouse click,
|
|
326
327
|
responsive layout. Tested on Windows with WezTerm; Linux/macOS should
|
package/package.json
CHANGED
package/src/input/handleKey.ts
CHANGED
|
@@ -174,6 +174,15 @@ export function handleKey(
|
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// Manual full refresh: re-read boards from disk, rescan agents, force-
|
|
178
|
+
// refetch the agenda calendar (bypassing its 30-min cache). For pulling in
|
|
179
|
+
// external changes — e.g. a calendar event edited in the browser — without
|
|
180
|
+
// restarting. Shift+R is ignored here so it stays free for future use.
|
|
181
|
+
if (key.name === "r" && !key.ctrl && !key.shift) {
|
|
182
|
+
store.refreshAll();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
// Zoom toggle: focus the active panel (board column or virtual panel)
|
|
178
187
|
// at full width.
|
|
179
188
|
if (key.name === "z") {
|
|
@@ -181,6 +190,35 @@ export function handleKey(
|
|
|
181
190
|
return;
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
// Agenda day navigation works from ANY zone — these keys only ever affect
|
|
194
|
+
// the Agenda, so there's no need to be focused there first. `[` previous
|
|
195
|
+
// day, `]` next day, `\` back to today. Pressing one also moves focus to
|
|
196
|
+
// the Agenda so you can keep paging/navigating. Guarded on the zone being
|
|
197
|
+
// visible — never steal focus to a hidden zone (F2 can hide it).
|
|
198
|
+
if (
|
|
199
|
+
ui.visibleZones.timeline &&
|
|
200
|
+
(key.name === "[" ||
|
|
201
|
+
key.sequence === "[" ||
|
|
202
|
+
key.name === "]" ||
|
|
203
|
+
key.sequence === "]" ||
|
|
204
|
+
key.name === "\\" ||
|
|
205
|
+
key.sequence === "\\")
|
|
206
|
+
) {
|
|
207
|
+
store.setActiveZone("timeline");
|
|
208
|
+
if (key.name === "\\" || key.sequence === "\\") {
|
|
209
|
+
store.resetAgendaDay();
|
|
210
|
+
store.flashBanner("info", "Agenda → Today");
|
|
211
|
+
} else {
|
|
212
|
+
const delta = key.name === "]" || key.sequence === "]" ? 1 : -1;
|
|
213
|
+
store.shiftAgendaDay(delta);
|
|
214
|
+
store.flashBanner(
|
|
215
|
+
"info",
|
|
216
|
+
`${delta > 0 ? "▶" : "◀"} ${formatAgendaDay(store.state.ui.agendaOffset, store.agendaDate())}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
184
222
|
// Defer modal opens by one macrotask so the OpenTUI <input> mounts after
|
|
185
223
|
// the current key event has been fully dispatched.
|
|
186
224
|
const openLater = (m: ModalKind) => {
|
|
@@ -247,26 +285,8 @@ function handleTimelineZone(
|
|
|
247
285
|
openLater: (m: ModalKind) => void,
|
|
248
286
|
): void {
|
|
249
287
|
const ui = store.state.ui;
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
// `[` previous, `]` next, `\` back to today. Handled first so they work
|
|
253
|
-
// regardless of arm/cursor state.
|
|
254
|
-
if (key.name === "[" || key.sequence === "[") {
|
|
255
|
-
store.shiftAgendaDay(-1);
|
|
256
|
-
store.flashBanner("info", `◀ ${formatAgendaDay(store.state.ui.agendaOffset, store.agendaDate())}`);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (key.name === "]" || key.sequence === "]") {
|
|
260
|
-
store.shiftAgendaDay(1);
|
|
261
|
-
store.flashBanner("info", `▶ ${formatAgendaDay(store.state.ui.agendaOffset, store.agendaDate())}`);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (key.name === "\\" || key.sequence === "\\") {
|
|
265
|
-
store.resetAgendaDay();
|
|
266
|
-
store.flashBanner("info", "Agenda → Today");
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
288
|
+
// Note: Agenda day-nav (`[` / `]` / `\`) is handled globally in handleKey
|
|
289
|
+
// before zone dispatch, so it works from any zone — not repeated here.
|
|
270
290
|
const entries = buildTimelineEntries(
|
|
271
291
|
store.state.boards.map((b) => b.board),
|
|
272
292
|
store.agendaDate(),
|
package/src/store/calendar.ts
CHANGED
|
@@ -143,9 +143,15 @@ async function googleAccessToken(tokenPath: string): Promise<string | null> {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
async function fetchGoogle(
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
async function fetchGoogle(
|
|
147
|
+
cfg: GoogleCalendarConfig,
|
|
148
|
+
dateIso: string,
|
|
149
|
+
force = false,
|
|
150
|
+
): Promise<CalEvent[]> {
|
|
151
|
+
if (!force) {
|
|
152
|
+
const cached = loadCache("google", dateIso);
|
|
153
|
+
if (cached) return cached;
|
|
154
|
+
}
|
|
149
155
|
|
|
150
156
|
const access = await googleAccessToken(cfg.token);
|
|
151
157
|
if (!access) return [];
|
|
@@ -304,9 +310,15 @@ function ensureUtc(s: string): string {
|
|
|
304
310
|
return `${s}Z`;
|
|
305
311
|
}
|
|
306
312
|
|
|
307
|
-
async function fetchMicrosoft(
|
|
308
|
-
|
|
309
|
-
|
|
313
|
+
async function fetchMicrosoft(
|
|
314
|
+
cfg: MicrosoftCalendarConfig,
|
|
315
|
+
dateIso: string,
|
|
316
|
+
force = false,
|
|
317
|
+
): Promise<CalEvent[]> {
|
|
318
|
+
if (!force) {
|
|
319
|
+
const cached = loadCache("microsoft", dateIso);
|
|
320
|
+
if (cached) return cached;
|
|
321
|
+
}
|
|
310
322
|
|
|
311
323
|
const access = await microsoftAccessToken(cfg);
|
|
312
324
|
if (!access) return [];
|
|
@@ -367,15 +379,16 @@ async function fetchMicrosoft(cfg: MicrosoftCalendarConfig, dateIso: string): Pr
|
|
|
367
379
|
export async function fetchCalendarEvents(
|
|
368
380
|
calendars: CalendarsConfig | undefined,
|
|
369
381
|
dateIso: string,
|
|
382
|
+
force = false,
|
|
370
383
|
): Promise<CalEvent[]> {
|
|
371
384
|
if (!calendars) return [];
|
|
372
385
|
const out: CalEvent[] = [];
|
|
373
386
|
const tasks: Array<Promise<CalEvent[]>> = [];
|
|
374
387
|
if (calendars.google?.enabled && calendars.google.token) {
|
|
375
|
-
tasks.push(fetchGoogle(calendars.google, dateIso));
|
|
388
|
+
tasks.push(fetchGoogle(calendars.google, dateIso, force));
|
|
376
389
|
}
|
|
377
390
|
if (calendars.microsoft?.enabled && calendars.microsoft.config && calendars.microsoft.tokenCache) {
|
|
378
|
-
tasks.push(fetchMicrosoft(calendars.microsoft, dateIso));
|
|
391
|
+
tasks.push(fetchMicrosoft(calendars.microsoft, dateIso, force));
|
|
379
392
|
}
|
|
380
393
|
for (const arr of await Promise.all(tasks)) out.push(...arr);
|
|
381
394
|
out.sort((a, b) => a.startMin - b.startMin);
|
|
@@ -389,8 +402,12 @@ export interface CalendarStore {
|
|
|
389
402
|
events: () => CalEvent[];
|
|
390
403
|
/** Switch which date's events `events()` exposes; fetches it (cache-first). */
|
|
391
404
|
setActiveDate: (dateIso: string) => void;
|
|
392
|
-
/**
|
|
393
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Re-fetch the active date now (the 5-min interval calls this too). Pass
|
|
407
|
+
* `force` to bypass the 30-min disk cache — used by the manual `r` refresh
|
|
408
|
+
* so freshly-edited events show without waiting for the cache to expire.
|
|
409
|
+
*/
|
|
410
|
+
refresh: (force?: boolean) => void;
|
|
394
411
|
dispose: () => Promise<void>;
|
|
395
412
|
}
|
|
396
413
|
|
|
@@ -409,10 +426,10 @@ export function createCalendarStore(
|
|
|
409
426
|
let activeDate = initialDate();
|
|
410
427
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
411
428
|
|
|
412
|
-
function refresh(): void {
|
|
429
|
+
function refresh(force = false): void {
|
|
413
430
|
if (!calendars) return;
|
|
414
431
|
const target = activeDate;
|
|
415
|
-
void fetchCalendarEvents(calendars, target)
|
|
432
|
+
void fetchCalendarEvents(calendars, target, force)
|
|
416
433
|
.then((evs) => {
|
|
417
434
|
// Guard against out-of-order resolves when the user pages quickly:
|
|
418
435
|
// only apply if this is still the date the user is looking at.
|
package/src/store/index.ts
CHANGED
|
@@ -819,6 +819,22 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
819
819
|
setState("ui", "armedTimelineRef", undefined);
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Manual full refresh (the `r` key). Re-reads every board from disk, rescans
|
|
824
|
+
* Claude Code agents, and force-refetches the Agenda's calendar (bypassing
|
|
825
|
+
* the 30-min cache). Lets the user pull in external changes — a calendar
|
|
826
|
+
* event edited in the browser, a board touched elsewhere — without leaving
|
|
827
|
+
* tuiboard. Boards normally auto-reload via the file watcher; this also
|
|
828
|
+
* covers the agenda, whose feed is poll-based, not event-driven.
|
|
829
|
+
*/
|
|
830
|
+
function refreshAll(): void {
|
|
831
|
+
setState("boards", loadAll(config));
|
|
832
|
+
setState("rev", (r) => r + 1);
|
|
833
|
+
agentsStore.refresh();
|
|
834
|
+
calendarStore.refresh(true);
|
|
835
|
+
flashBanner("info", "Refreshed boards · agents · agenda");
|
|
836
|
+
}
|
|
837
|
+
|
|
822
838
|
// ─── Multi-select ────────────────────────────────────────────────────────
|
|
823
839
|
|
|
824
840
|
function markKey(ref: TaskRef): string {
|
|
@@ -1027,6 +1043,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1027
1043
|
agendaDate,
|
|
1028
1044
|
shiftAgendaDay,
|
|
1029
1045
|
resetAgendaDay,
|
|
1046
|
+
refreshAll,
|
|
1030
1047
|
setFilter,
|
|
1031
1048
|
applyBoardFilter,
|
|
1032
1049
|
setZoomed,
|
package/src/ui/AgentRow.tsx
CHANGED
|
@@ -70,11 +70,13 @@ export function AgentRow(props: AgentRowProps) {
|
|
|
70
70
|
<span style={{ fg: T.textDim }}>{" "}{props.session.gitBranch}</span>
|
|
71
71
|
</Show>
|
|
72
72
|
</text>
|
|
73
|
-
{/* cwd + age pinned together on the right
|
|
74
|
-
|
|
73
|
+
{/* cwd + age pinned together on the right. The age is right-aligned in a
|
|
74
|
+
fixed-width field (pad to 3: "59m" / "23h" / "10d") so the END of each
|
|
75
|
+
cwd lands on the same column across rows — a 1- vs 2-digit age no
|
|
76
|
+
longer shoves the directory names out of vertical alignment. */}
|
|
75
77
|
<text style={{ flexShrink: 0 }} wrapMode="none">
|
|
76
78
|
<span style={{ fg: T.textDim }}>
|
|
77
|
-
{props.session.cwdShort}{"
|
|
79
|
+
{props.session.cwdShort}{" "}{ageStr().padStart(3)}
|
|
78
80
|
</span>
|
|
79
81
|
</text>
|
|
80
82
|
</box>
|
package/src/ui/Chrome.tsx
CHANGED
|
@@ -115,7 +115,7 @@ export function BottomBar(props: { store: TuiStore }) {
|
|
|
115
115
|
*/}
|
|
116
116
|
<text wrapMode="none" truncate>
|
|
117
117
|
<span style={{ fg: T.textDim }}>
|
|
118
|
-
{"hjkl move · Tab board · ⇧Tab zone · ⏎ done · n new · t today · b block · c schedule · z zoom · ? help · q quit"}
|
|
118
|
+
{"hjkl ↑↓←→ move · Tab board · ⇧Tab zone · ⏎ done · n new · t today · b block · c schedule · r refresh · z zoom · ? help · q quit"}
|
|
119
119
|
</span>
|
|
120
120
|
</text>
|
|
121
121
|
</box>
|
package/src/ui/Modal.tsx
CHANGED
|
@@ -592,6 +592,7 @@ function HelpModal(props: { store: TuiStore }) {
|
|
|
592
592
|
<span style={{ fg: T.text }}>{" Shift-Tab Cycle active zone (virtual → board → timeline → agents)\n"}</span>
|
|
593
593
|
<span style={{ fg: T.text }}>{" F1 / F2 / F3 Toggle visibility of Virtual / Timeline / Agents zones\n"}</span>
|
|
594
594
|
<span style={{ fg: T.text }}>{" z Zoom active zone (or column) to full screen\n"}</span>
|
|
595
|
+
<span style={{ fg: T.text }}>{" r Refresh everything (boards from disk, agents, agenda calendar)\n"}</span>
|
|
595
596
|
<span style={{ fg: T.textDim }}>{"\nTask actions (work in board, virtual, AND timeline zones)\n"}</span>
|
|
596
597
|
<span style={{ fg: T.text }}>{" Enter Toggle done\n"}</span>
|
|
597
598
|
<span style={{ fg: T.text }}>{" o Open detail view\n"}</span>
|
|
@@ -606,7 +607,7 @@ function HelpModal(props: { store: TuiStore }) {
|
|
|
606
607
|
<span style={{ fg: T.text }}>{" d Delete task (with confirm)\n"}</span>
|
|
607
608
|
<span style={{ fg: T.text }}>{" X Archive task → moves to Archive column\n"}</span>
|
|
608
609
|
<span style={{ fg: T.text }}>{" C Copy task to clipboard (markdown line)\n"}</span>
|
|
609
|
-
<span style={{ fg: T.textDim }}>{"\nAgenda (timeline) day navigation\n"}</span>
|
|
610
|
+
<span style={{ fg: T.textDim }}>{"\nAgenda (timeline) day navigation — works from any zone\n"}</span>
|
|
610
611
|
<span style={{ fg: T.text }}>{" [ / ] Previous / next day (tasks + calendar events)\n"}</span>
|
|
611
612
|
<span style={{ fg: T.text }}>{" \\ Jump back to today\n"}</span>
|
|
612
613
|
<span style={{ fg: T.textDim }}>{"\nAgenda (timeline) scheduling\n"}</span>
|
package/src/ui/TimelineView.tsx
CHANGED
|
@@ -335,10 +335,14 @@ export function TimelineView(props: TimelineViewProps) {
|
|
|
335
335
|
</span>
|
|
336
336
|
</text>
|
|
337
337
|
</Show>
|
|
338
|
-
|
|
338
|
+
{/* Day-navigation hint — always visible in the resting state (not while
|
|
339
|
+
arming) so the [ ] day-switch is discoverable. Off-today, the
|
|
340
|
+
"\ today" reset is highlighted to pull the eye back. */}
|
|
341
|
+
<Show when={!armMode() && !armedTask()}>
|
|
339
342
|
<text wrapMode="none">
|
|
340
343
|
<span style={{ fg: T.warm }}>{"◷ "}</span>
|
|
341
|
-
<span style={{ fg: T.textDim }}>{"[
|
|
344
|
+
<span style={{ fg: T.textDim }}>{"[ ] change day · "}</span>
|
|
345
|
+
<span style={{ fg: isToday() ? T.textDim : T.warm }}>{"\\ today"}</span>
|
|
342
346
|
</text>
|
|
343
347
|
</Show>
|
|
344
348
|
<Show when={!armedTask() && rowMap().overflow > 0}>
|