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 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) and day-navigation (`[` / `]` / `\`) so you can
323
- page tasks and events across days.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Terminal dashboard for markdown task boards. Kanban + Today/Tomorrow + 24h timeline + Claude Code agent view, all in one TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- // Day navigation: page the Agenda to other days (tasks + calendar events).
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(),
@@ -143,9 +143,15 @@ async function googleAccessToken(tokenPath: string): Promise<string | null> {
143
143
  }
144
144
  }
145
145
 
146
- async function fetchGoogle(cfg: GoogleCalendarConfig, dateIso: string): Promise<CalEvent[]> {
147
- const cached = loadCache("google", dateIso);
148
- if (cached) return cached;
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(cfg: MicrosoftCalendarConfig, dateIso: string): Promise<CalEvent[]> {
308
- const cached = loadCache("microsoft", dateIso);
309
- if (cached) return cached;
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
- /** Re-fetch the active date now (the 5-min interval calls this too). */
393
- refresh: () => void;
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.
@@ -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,
@@ -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, so the session titles align
74
- cleanly on the left instead of being pushed around by the path. */}
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}{" "}{ageStr()}
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>
@@ -335,10 +335,14 @@ export function TimelineView(props: TimelineViewProps) {
335
335
  </span>
336
336
  </text>
337
337
  </Show>
338
- <Show when={!isToday()}>
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 }}>{"[ prev · ] next · \\ today"}</span>
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}>