tuiboard 0.7.0 → 0.7.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,106 @@
1
+ # Changelog
2
+
3
+ All notable changes to **tuiboard** are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.7.2] - 2026-06-02
9
+
10
+ ### Changed
11
+ - Documentation: the intro and npm description now lead with the modular pitch
12
+ (a kanban with three optional panels you switch on or off) instead of a
13
+ bundled four-zone dashboard.
14
+
15
+ ## [0.7.1] - 2026-06-02
16
+
17
+ ### Changed
18
+ - Modals (new task, schedule, time block, assign, delete, detail, search, help)
19
+ now open in the Agenda's slot — an opaque panel of the same width — instead of
20
+ a side panel that pushed the dashboard left. Opening a modal no longer reflows
21
+ the board/planner; the Agenda returns when the modal closes.
22
+ - Modal titles now ride in the panel's top border (`┤ … ├`), matching the board
23
+ columns and the zones, instead of sitting as a body text line.
24
+ - A clipped board column keeps its task rows until it's scrolled down to less
25
+ than half visible (previously: blanked as soon as it was clipped at all), so a
26
+ column that's mostly on-screen stays useful.
27
+
28
+ ## [0.7.0] - 2026-06-01
29
+
30
+ ### Added
31
+ - **Configurable zones.** A `zones:` config block turns the planner, agenda, or
32
+ agents view off (`off`), starts it collapsed (`hidden`), or leaves it on
33
+ (`on`, the default; `true`/`false` alias `on`/`off`). The board is always on.
34
+ tuiboard can now be a pure kanban, kanban + calendar, kanban + agents, or any
35
+ mix. A disabled zone is never rendered, is skipped by `Shift-Tab`, has an
36
+ inert F-key, and **its background work never starts** — no calendar fetch and
37
+ no `~/.claude` reads when the agents zone is off.
38
+ - Documented zones in the README, the AI setup prompt, and `config.example.yaml`.
39
+
40
+ ### Changed
41
+ - Renamed the internal "virtual" zone to **"planner"** throughout (code,
42
+ identifiers, comments, and the `VirtualPanel`/`virtual-panel` files) for
43
+ clarity. The visible "Today/Tomorrow" panel is unchanged.
44
+ - Reworked the responsive layout to combine three inputs — `enabled ∧ desired ∧
45
+ fits-width`. Auto-hide now only reports what fits; it never force-shows a
46
+ disabled or intentionally-hidden zone, and `F1`/`F2`/`F3` toggles persist
47
+ across terminal resizes.
48
+
49
+ ## [0.6.2] - 2026-05-30
50
+
51
+ ### Changed
52
+ - Updated the hero screenshot to show the live calendar overlay (Google +
53
+ Microsoft 365 events side by side) and the aligned agent rows.
54
+ - Refreshed the README intro to mention the calendar overlay.
55
+
56
+ ## [0.6.1] - 2026-05-30
57
+
58
+ ### Added
59
+ - **Manual full-refresh key (`r`).** Reloads boards from disk, rescans agents,
60
+ and force-refetches the agenda calendar (bypassing the 30-minute cache) so
61
+ externally-edited events show without a restart.
62
+
63
+ ### Changed
64
+ - Day-navigation keys (`[` / `]` / `\`) now work from any zone, not just when
65
+ the agenda is focused; pressing one also moves focus to the agenda.
66
+ - Added arrow keys and `r refresh` to the bottom cheat-sheet; the day-navigation
67
+ hint is now always visible in the agenda's resting state.
68
+ - Agent rows right-align the activity age in a fixed-width field so the end of
69
+ each working directory lines up across rows.
70
+
71
+ ## [0.6.0] - 2026-05-30
72
+
73
+ First public release on npm. This entry captures the full feature set at launch.
74
+
75
+ ### Added
76
+ - **Kanban board** over plain CommonMark files using the Obsidian Tasks-plugin
77
+ emoji vocabulary — no lock-in, the files stay yours. Multiple boards as tabs;
78
+ `##` headings become columns; `Done` and `Archive` columns are treated
79
+ specially. Quick-add syntax (`@assignee`, `#tag`, scheduling, time blocks,
80
+ priority), multi-select (`Space`), undo (`Ctrl-Z`), filters, search (`/`),
81
+ zoom (`z`), and atomic file round-trips with an external-edit watcher.
82
+ - **Planner** — a Today/Tomorrow panel aggregating everything scheduled across
83
+ all boards.
84
+ - **Agenda** — a 24-hour timeline with click-to-arm time-blocking, plus a
85
+ read-only **calendar overlay** for Google Calendar and Microsoft 365
86
+ (dependency-light, bring-your-own-credentials, all-day events skipped, each
87
+ calendar in its own color). Day-navigation with `[` / `]` / `\` pages tasks
88
+ and events across days.
89
+ - **`tuiboard calendar-setup`** — one-time OAuth for new users (Google browser
90
+ flow, Microsoft device-code flow); prints the exact `calendars:` block to add.
91
+ - **Live agents view** — reads local Claude Code sessions from `~/.claude` with
92
+ zero setup, showing status, branch, and last activity. `Enter` opens a session
93
+ in a terminal; the launch command is overridable via `resume_command`.
94
+ - **Keyboard-first with full mouse support**, a responsive multi-zone layout
95
+ that adapts to terminal width, standalone `--view=` modes, and the `tb` alias.
96
+ - Config resolution via `$TUIBOARD_CONFIG`, a project-local `.tuiboard/`, the
97
+ global `~/.config/tuiboard/`, or a cwd fallback scan.
98
+
99
+ Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
100
+
101
+ [0.7.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.2
102
+ [0.7.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.1
103
+ [0.7.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.0
104
+ [0.6.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.6.2
105
+ [0.6.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.6.1
106
+ [0.6.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.6.0
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # tuiboard
2
2
 
3
- A terminal dashboard that unifies **kanban**, a **Today/Tomorrow planner
4
- panel**, a **24-hour agenda** (with a read-only Google / Microsoft 365
5
- calendar overlay), and a **live agent view** for Claude Code sessions — all
6
- on top of plain markdown task files.
3
+ A terminal **kanban** board on plain markdown files, with three optional panels
4
+ you switch on or off: a **Today/Tomorrow planner** across all your boards, a
5
+ **24-hour agenda** with a read-only Google / Microsoft 365 calendar overlay, and
6
+ a **live view of your Claude Code sessions**. Run it as a pure kanban, or any mix
7
+ of the four. The board is always on; the rest is opt-in (see [Zones](#zones)).
7
8
 
8
9
  Built with [OpenTUI](https://opentui.com) + SolidJS on Bun. Cross-platform
9
10
  (Linux, macOS, Windows). No vendor lock-in: boards are CommonMark with
@@ -269,11 +270,11 @@ are rebuilt only after an in-app mutation.
269
270
 
270
271
  ## Layouts
271
272
 
272
- Launch `tuiboard` with no flag for the default 4-zone dashboard.
273
+ Launch `tuiboard` with no flag for the default dashboard (every enabled zone).
273
274
 
274
275
  | Flag | View | Use case |
275
276
  |---|---|---|
276
- | (none) | **Dashboard** — all 4 zones | Default; everything in one terminal |
277
+ | (none) | **Dashboard** — every enabled zone | Default; your configured layout |
277
278
  | `--view=board` | Kanban + planner panel only | Focus mode, or a single WezTerm pane |
278
279
  | `--view=timeline` | Timeline fullscreen | Wall-mounted "what's now" |
279
280
  | `--view=agents` | Agent view fullscreen | Cross-machine session monitor |
@@ -353,6 +354,8 @@ session (until the next terminal resize).
353
354
 
354
355
  ## Status
355
356
 
357
+ See [CHANGELOG.md](CHANGELOG.md) for the full release history.
358
+
356
359
  - **v0.7** — configurable zones: turn the planner, agenda, or agents view off
357
360
  (or start it collapsed) via the `zones:` config, so tuiboard can be a pure
358
361
  kanban, kanban + calendar, or any mix.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.7.0",
4
- "description": "Terminal dashboard for markdown task boards. Kanban + Today/Tomorrow + 24h timeline + Claude Code agent view, all in one TUI.",
3
+ "version": "0.7.2",
4
+ "description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Nazzareno Giannelli <nazzareno.giannelli@gmail.com>",
@@ -36,6 +36,7 @@
36
36
  "bin/",
37
37
  ".tuiboard/config.example.yaml",
38
38
  "README.md",
39
+ "CHANGELOG.md",
39
40
  "LICENSE"
40
41
  ],
41
42
  "scripts": {
package/src/app.tsx CHANGED
@@ -121,11 +121,11 @@ function App() {
121
121
  <TopBar store={store} />
122
122
  <box style={{ height: 1 }} />
123
123
  {/*
124
- rootView and ModalLayer are siblings inside a flex-row so the
125
- modal can sit beside the view as a fixed-width, full-height
126
- side panel. ModalLayer renders only when ui.modal is set,
127
- otherwise its <Show> resolves to nothing and the rootView gets
128
- the whole row.
124
+ rootView + ModalLayer are flex-row siblings. The modal panel is exactly
125
+ the Agenda's width, and the Dashboard hides the Agenda while a modal is
126
+ open so the modal drops into the Agenda's slot with zero reflow of the
127
+ left side (board / planner / agents). When no modal is open ModalLayer
128
+ renders nothing and rootView gets the whole row.
129
129
  */}
130
130
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
131
131
  <box style={{ flexDirection: "column", flexGrow: 1 }}>
@@ -31,6 +31,13 @@ interface SizedBoxLike {
31
31
 
32
32
  /** Fixed column width when not zoomed. Single source of truth for layout. */
33
33
  const COL_WIDTH = 42;
34
+ /**
35
+ * Minimum fraction of a column that must be inside the viewport for its task
36
+ * rows to render. Above this, a column that's mostly on-screen keeps its tasks
37
+ * (the last one may be cut at the edge); below it — a thin sliver clipped at the
38
+ * edge — only the title shows as a "more columns →" hint.
39
+ */
40
+ const MIN_TASKS_VISIBLE_FRACTION = 0.5;
34
41
  /**
35
42
  * Width of a collapsed column — one with no OPEN tasks (an all-done lane like
36
43
  * "Done", or an empty column). It shows just the `✓ N` counter; zoom (`z`)
@@ -140,16 +147,22 @@ export function BoardView(props: BoardViewProps) {
140
147
  });
141
148
 
142
149
  /**
143
- * Is the column at rendered index `i` fully inside the viewport? Used to
144
- * blank the task rows of a column that's only partly on-screen. Defaults to
145
- * true while the viewport width is still unknown (first paint) and in zoom.
150
+ * Should the column at rendered index `i` render its task rows? Yes unless
151
+ * it's clipped down to a thin sliver at the viewport edge (less than
152
+ * MIN_TASKS_VISIBLE_FRACTION on-screen) then only its title shows as a
153
+ * "more columns →" hint. A mostly-visible column keeps its tasks even if the
154
+ * last one is cut at the edge. Defaults to true while the viewport width is
155
+ * still unknown (first paint) and in zoom.
146
156
  */
147
- const columnFullyVisible = (i: number): boolean => {
157
+ const columnTasksVisible = (i: number): boolean => {
148
158
  const vw = viewportW();
149
159
  if (vw <= 0 || ui().zoomed) return true;
150
160
  const stride = COL_WIDTH + COL_GAP;
151
161
  const start = i * stride;
152
- return start >= scrollX() && start + COL_WIDTH <= scrollX() + vw;
162
+ const left = Math.max(start, scrollX());
163
+ const right = Math.min(start + COL_WIDTH, scrollX() + vw);
164
+ const visibleFraction = Math.max(0, right - left) / COL_WIDTH;
165
+ return visibleFraction >= MIN_TASKS_VISIBLE_FRACTION;
153
166
  };
154
167
 
155
168
  // Measure the viewport width once at mount, regardless of the active zone.
@@ -199,7 +212,7 @@ export function BoardView(props: BoardViewProps) {
199
212
  columnIndex={originalIndex}
200
213
  active={isActive()}
201
214
  zoomed={ui().zoomed && isActive()}
202
- fullyVisible={columnFullyVisible(i())}
215
+ tasksVisible={columnTasksVisible(i())}
203
216
  boxId={columnId(props.board.filepath, originalIndex)}
204
217
  />
205
218
  );
@@ -223,11 +236,11 @@ interface ColumnViewProps {
223
236
  */
224
237
  zoomed: boolean;
225
238
  /**
226
- * False when the column is only partly on-screen (clipped by horizontal
227
- * scroll). Its title still renders (clipped) as a "more columns" hint, but
228
- * the task rows are blanked so no half-cut task text shows.
239
+ * False when the column is clipped down to a thin sliver at the viewport
240
+ * edge. Its title still renders as a "more columns" hint, but the task rows
241
+ * are blanked. Mostly-visible columns keep their tasks.
229
242
  */
230
- fullyVisible?: boolean;
243
+ tasksVisible?: boolean;
231
244
  /** Stable DOM-equivalent id used by `scrollChildIntoView`. */
232
245
  boxId: string;
233
246
  }
@@ -349,9 +362,9 @@ function ColumnView(props: ColumnViewProps) {
349
362
  scrollbarOptions: { visible: false },
350
363
  }}
351
364
  >
352
- {/* Blank the task rows when the column is only partly on-screen its
353
- (clipped) title still shows as a "more columns" hint. */}
354
- <Show when={props.fullyVisible !== false}>
365
+ {/* Blank the task rows only when the column is a thin clipped sliver
366
+ its title still shows as a "more columns" hint. */}
367
+ <Show when={props.tasksVisible !== false}>
355
368
  {/*
356
369
  Keyed on the task-list signature so a structural change (add/delete/
357
370
  move) rebuilds the <For> fresh in the correct order, working around
package/src/ui/Modal.tsx CHANGED
@@ -20,41 +20,21 @@ import {
20
20
  parseTimeBlockShortcut,
21
21
  } from "~/store/parsers";
22
22
  import { ATTR, T } from "~/ui/glyphs";
23
+ import { TIMELINE_WIDTH } from "~/views/Dashboard";
23
24
  import type { TuiStore } from "~/store/index";
24
25
  import type { PriorityLevel, TimeBlock } from "~/types";
25
26
 
26
- /** Fixed side-panel width for any modal. Wide enough for Detail / Help
27
- * without being absurd for Edit / Confirm. */
28
- const MODAL_WIDTH = 64;
27
+ /** The modal panel matches the Agenda's width so it can drop into the Agenda's
28
+ * slot (which the Dashboard vacates while a modal is open) with no reflow. */
29
+ const MODAL_WIDTH = TIMELINE_WIDTH;
29
30
 
30
31
  export function ModalLayer(props: { store: TuiStore }) {
31
32
  const modal = createMemo(() => props.store.state.ui.modal);
32
33
  return (
33
34
  <Show when={modal()}>
34
- <box
35
- style={{
36
- flexDirection: "column",
37
- width: MODAL_WIDTH,
38
- minWidth: MODAL_WIDTH,
39
- flexGrow: 0,
40
- flexShrink: 0,
41
- // Stretch to the parent's height so the modal panel matches
42
- // the timeline / board zones it sits next to. Same contract as
43
- // every other zone — fixed cross-axis size, full-height fill.
44
- alignSelf: "stretch",
45
- marginLeft: 1,
46
- backgroundColor: T.panelBgActive,
47
- border: true,
48
- borderStyle: "rounded",
49
- borderColor: T.borderActive,
50
- paddingLeft: 1,
51
- paddingRight: 1,
52
- paddingTop: 1,
53
- paddingBottom: 1,
54
- }}
55
- >
56
- <ModalRouter store={props.store} modal={modal()!} />
57
- </box>
35
+ {/* Each modal's DialogShell IS the panel box (border + title + slot
36
+ dimensions), so it drops into the Agenda's slot directly. */}
37
+ <ModalRouter store={props.store} modal={modal()!} />
58
38
  </Show>
59
39
  );
60
40
  }
@@ -87,11 +67,31 @@ interface DialogShellProps {
87
67
  function DialogShell(props: DialogShellProps) {
88
68
  void props.width;
89
69
  return (
90
- <box style={{ flexDirection: "column" }}>
91
- <text>
92
- <span style={{ fg: T.accent, attributes: ATTR.bold }}>{props.title}</span>
93
- </text>
94
- <box style={{ height: 1 }} />
70
+ <box
71
+ style={{
72
+ // The modal panel: same width + marginLeft as the Agenda so it occupies
73
+ // the Agenda's slot (the Dashboard hides the Agenda while a modal is
74
+ // open) with no reflow. The title rides in the top border, exactly like
75
+ // the board columns and the other zones.
76
+ flexDirection: "column",
77
+ width: MODAL_WIDTH,
78
+ minWidth: MODAL_WIDTH,
79
+ flexGrow: 0,
80
+ flexShrink: 0,
81
+ alignSelf: "stretch",
82
+ marginLeft: 1,
83
+ backgroundColor: T.panelBgActive,
84
+ border: true,
85
+ borderStyle: "rounded",
86
+ borderColor: T.borderActive,
87
+ paddingLeft: 1,
88
+ paddingRight: 1,
89
+ paddingTop: 1,
90
+ paddingBottom: 1,
91
+ }}
92
+ title={`┤ ${props.title} ├`}
93
+ titleAlignment="left"
94
+ >
95
95
  {props.children}
96
96
  <Show when={props.hint}>
97
97
  <text>
@@ -207,11 +207,7 @@ function ScheduleModal(props: { store: TuiStore; modal: Extract<NonNullable<TuiS
207
207
 
208
208
  return (
209
209
  <DialogShell
210
- title={
211
- markedCount > 1
212
- ? `Schedule ${markedCount} selected tasks`
213
- : `Schedule: ${task?.displayTitle.slice(0, 50) ?? ""}`
214
- }
210
+ title={markedCount > 1 ? `Schedule · ${markedCount} tasks` : "Schedule"}
215
211
  hint="t = today · tm = tomorrow · +3 = in 3 days · lun = next Monday · 2026-06-15 · empty/-clear · Esc to cancel"
216
212
  width={70}
217
213
  >
@@ -256,11 +252,7 @@ function TimeBlockModal(props: { store: TuiStore; modal: Extract<NonNullable<Tui
256
252
 
257
253
  return (
258
254
  <DialogShell
259
- title={
260
- markedCount > 1
261
- ? `Time block ${markedCount} selected tasks`
262
- : `Time block: ${task?.displayTitle.slice(0, 50) ?? ""}`
263
- }
255
+ title={markedCount > 1 ? `Time block · ${markedCount} tasks` : "Time block"}
264
256
  hint="n = now+30 · 9:00 · 9-11 · 09:30-10:45 · - to clear · Esc to cancel"
265
257
  width={70}
266
258
  >
@@ -30,8 +30,12 @@ import { BoardOnly } from "~/views/BoardOnly";
30
30
  import { TimelineOnly } from "~/views/TimelineOnly";
31
31
  import type { TuiStore } from "~/store/index";
32
32
 
33
- /** Width (in cells) for the right-column Timeline panel on a wide terminal. */
34
- const TIMELINE_WIDTH = 50;
33
+ /**
34
+ * Width (in cells) for the right-column Agenda panel on a wide terminal. The
35
+ * modal panel matches this so it can drop into the Agenda's slot (see
36
+ * ModalLayer) without reflowing the rest of the dashboard.
37
+ */
38
+ export const TIMELINE_WIDTH = 50;
35
39
  /** Row height for the bottom Agents strip — enough for ~5 sessions. */
36
40
  const AGENTS_HEIGHT = 7;
37
41
 
@@ -96,8 +100,9 @@ function FourZoneLayout(props: { store: TuiStore }) {
96
100
  <AgentsBar store={props.store} height={AGENTS_HEIGHT} />
97
101
  </Show>
98
102
  </box>
99
- {/* Right column: timeline (full height) */}
100
- <Show when={visible().timeline}>
103
+ {/* Right column: Agenda (full height). Hidden while a modal is open so
104
+ the modal panel takes its slot with no reflow. */}
105
+ <Show when={visible().timeline && !ui().modal}>
101
106
  <TimelineView store={props.store} width={TIMELINE_WIDTH} />
102
107
  </Show>
103
108
  {/* ModalLayer rendered at App level so it can sit beside any view */}