tuiboard 0.6.2 → 0.7.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.
@@ -31,6 +31,20 @@ done_column: Done
31
31
  # doesn't exist in a board, tuiboard creates it on the fly.
32
32
  archive_column: Archive
33
33
 
34
+ # Optional: pick which zones you want. The board is always on; the other three
35
+ # are yours to turn off. Each takes one of:
36
+ # on enabled and shown at launch (the default)
37
+ # off disabled entirely — never rendered, skipped by Shift-Tab, its F-key
38
+ # is inert, and its background work never runs (no calendar fetch, no
39
+ # ~/.claude reads). Use this for a pure kanban, or kanban + one panel.
40
+ # hidden enabled but collapsed at launch — reveal it any time with its F-key
41
+ # Want just a kanban? Set agenda and agents to off. (true/false also work.)
42
+ #
43
+ # zones:
44
+ # planner: on # Today/Tomorrow cross-board panel (F1)
45
+ # agenda: on # 24h agenda + calendar overlay (F2)
46
+ # agents: on # live Claude Code session view (F3)
47
+
34
48
  # Optional: override what Enter does in the Agents zone ("open the selected
35
49
  # Claude Code session"). An argv array — the tokens {cwd} and {sessionId} are
36
50
  # substituted, then it's run directly (no shell). Point it at your own script
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # tuiboard
2
2
 
3
- A terminal dashboard that unifies **kanban**, a **Today/Tomorrow virtual
3
+ A terminal dashboard that unifies **kanban**, a **Today/Tomorrow planner
4
4
  panel**, a **24-hour agenda** (with a read-only Google / Microsoft 365
5
5
  calendar overlay), and a **live agent view** for Claude Code sessions — all
6
6
  on top of plain markdown task files.
@@ -98,12 +98,17 @@ https://github.com/NazzarenoGiannelli/tuiboard). Set it up for me from scratch:
98
98
  3. Create a global config at `~/.config/tuiboard/config.yaml` (resolve the real
99
99
  home path) with a `boards:` list pointing at those files by ABSOLUTE path,
100
100
  plus `assignees: [...]`, `done_column: Done`, `archive_column: Archive`.
101
- 4. Do NOT configure the Agent view tuiboard reads `~/.claude` automatically.
101
+ 4. Ask me which zones I want besides the kanban board: the Today/Tomorrow
102
+ planner, the 24h agenda, and the live Claude Code agents view. For any I
103
+ don't want, add a `zones:` block setting it to `off` (e.g. someone who
104
+ doesn't use Claude Code would set `agents: off`). If I want them all, omit
105
+ the block. Do NOT configure the agents view beyond on/off — it reads
106
+ `~/.claude` automatically when enabled.
102
107
  5. Ask me whether I want to overlay my Google Calendar or Microsoft 365 events
103
- on the Agenda. If yes, tell me to run `tuiboard calendar-setup google` (or
104
- `microsoft`) — it interviews me, opens the browser, and prints the exact
105
- `calendars:` YAML block to add. Don't try to do the OAuth yourself. If no,
106
- skip it (it's optional and can be added later).
108
+ on the Agenda (skip this if I turned the agenda off). If yes, tell me to run
109
+ `tuiboard calendar-setup google` (or `microsoft`) — it interviews me, opens
110
+ the browser, and prints the exact `calendars:` YAML block to add. Don't try
111
+ to do the OAuth yourself. If no, skip it (it's optional and can be added later).
107
112
  6. Show me the final config, then tell me to run `tuiboard`, and how to add a
108
113
  board later (create a new `.md` and append it to the `boards:` list).
109
114
 
@@ -143,6 +148,34 @@ archive_column: Archive
143
148
  # resume_command: ["nu", "C:/Users/you/.config/tuiboard/code-resume.nu", "{cwd}", "{sessionId}"]
144
149
  ```
145
150
 
151
+ ## Zones
152
+
153
+ tuiboard is four zones — **board** (kanban), **planner** (Today/Tomorrow across
154
+ all boards), **agenda** (24h timeline + calendar overlay), and **agents** (live
155
+ Claude Code sessions). Only want some of them? The board is always on; the other
156
+ three are yours to configure:
157
+
158
+ ```yaml
159
+ zones:
160
+ planner: on # Today/Tomorrow panel (toggle at runtime with F1)
161
+ agenda: on # 24h agenda + calendars (F2)
162
+ agents: off # live Claude Code view (F3)
163
+ ```
164
+
165
+ Each zone takes one of:
166
+
167
+ | Value | Behavior |
168
+ |---|---|
169
+ | `on` | Enabled and shown at launch (the default). |
170
+ | `off` | **Disabled entirely** — never rendered, skipped by `Shift-Tab`, its F-key is inert, and its background work never starts (no calendar fetch, no `~/.claude` reads). |
171
+ | `hidden` | Enabled but **collapsed at launch** — reveal it any time with its F-key. |
172
+
173
+ `true`/`false` work as aliases for `on`/`off`. So a pure kanban is just
174
+ `agenda: off` and `agents: off`; kanban + calendar is `agents: off`. The
175
+ difference between `off` and the F-key hide: `off` means the feature never runs
176
+ at all — handy if you don't use Claude Code and don't want tuiboard reading
177
+ `~/.claude`.
178
+
146
179
  ## Calendars (Agenda overlay)
147
180
 
148
181
  The **Agenda** zone (the 24h timeline) can overlay read-only events from Google
@@ -241,7 +274,7 @@ Launch `tuiboard` with no flag for the default 4-zone dashboard.
241
274
  | Flag | View | Use case |
242
275
  |---|---|---|
243
276
  | (none) | **Dashboard** — all 4 zones | Default; everything in one terminal |
244
- | `--view=board` | Kanban + virtual panel only | Focus mode, or a single WezTerm pane |
277
+ | `--view=board` | Kanban + planner panel only | Focus mode, or a single WezTerm pane |
245
278
  | `--view=timeline` | Timeline fullscreen | Wall-mounted "what's now" |
246
279
  | `--view=agents` | Agent view fullscreen | Cross-machine session monitor |
247
280
 
@@ -249,9 +282,9 @@ The dashboard auto-collapses optional zones on narrow terminals:
249
282
 
250
283
  | Terminal width | Default zones visible |
251
284
  |---|---|
252
- | ≥ 150 cols | virtual + board + timeline + agents |
253
- | 120–149 | virtual + board + agents |
254
- | 100–119 | virtual + board |
285
+ | ≥ 150 cols | planner + board + timeline + agents |
286
+ | 120–149 | planner + board + agents |
287
+ | 100–119 | planner + board |
255
288
  | < 100 | board only |
256
289
 
257
290
  `F1` / `F2` / `F3` toggles override the auto-collapse for the current
@@ -266,9 +299,9 @@ session (until the next terminal resize).
266
299
  | `h j k l` / arrows | Move cursor inside the active zone |
267
300
  | `Tab` | Cycle to next board |
268
301
  | `1`..`9` | Jump to board N |
269
- | `v` | Toggle Today/Tomorrow virtual panel focus |
270
- | `Shift-Tab` | Cycle active zone (virtual → board → timeline → agents) |
271
- | `F1` / `F2` / `F3` | Toggle visibility of Virtual / Timeline / Agents zones |
302
+ | `v` | Toggle Today/Tomorrow planner panel focus |
303
+ | `Shift-Tab` | Cycle active zone (planner → board → timeline → agents) |
304
+ | `F1` / `F2` / `F3` | Toggle visibility of Planner / Timeline / Agents zones |
272
305
  | `z` | Zoom active zone to full screen |
273
306
  | `r` | Refresh everything — reload boards from disk, rescan agents, force-refetch the agenda calendar (bypasses the 30-min cache) |
274
307
 
@@ -282,7 +315,7 @@ session (until the next terminal resize).
282
315
  | `j` / `k` | While armed: nudge the block ±15 min |
283
316
  | `+` / `-` | While armed: resize the block's end ±15 min |
284
317
 
285
- ### Task actions (work in board, virtual, AND timeline zones)
318
+ ### Task actions (work in board, planner, AND timeline zones)
286
319
 
287
320
  | Key | Action |
288
321
  |---|---|
@@ -320,10 +353,13 @@ session (until the next terminal resize).
320
353
 
321
354
  ## Status
322
355
 
356
+ - **v0.7** — configurable zones: turn the planner, agenda, or agents view off
357
+ (or start it collapsed) via the `zones:` config, so tuiboard can be a pure
358
+ kanban, kanban + calendar, or any mix.
323
359
  - **v0.6** — adds the Agenda calendar overlay (Google + Microsoft 365,
324
360
  read-only, BYO credentials), day-navigation (`[` / `]` / `\`) to page tasks
325
361
  and events across days, and a manual full-refresh key (`r`).
326
- - **v0.5** — daily-driver ready. Kanban + virtual + timeline + agents
362
+ - **v0.5** — daily-driver ready. Kanban + planner + timeline + agents
327
363
  all functional, multi-select, undo, atomic file roundtrip, mouse click,
328
364
  responsive layout. Tested on Windows with WezTerm; Linux/macOS should
329
365
  work via the same OpenTUI binaries (untested).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
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",
package/src/app.tsx CHANGED
@@ -4,7 +4,7 @@
4
4
  * Loads config, builds the reactive store, parses argv, and dispatches
5
5
  * to one of four root views:
6
6
  * - undefined → Dashboard (all 4 zones)
7
- * - "board" → BoardOnly (kanban + virtual fullscreen)
7
+ * - "board" → BoardOnly (kanban + planner fullscreen)
8
8
  * - "timeline"→ TimelineOnly
9
9
  * - "agents" → AgentsOnly
10
10
  *
@@ -22,7 +22,7 @@ import {
22
22
  createTuiStore,
23
23
  type TuiStore,
24
24
  } from "~/store/index";
25
- import { buildVirtualItems } from "~/store/virtual-panel";
25
+ import { buildPlannerItems } from "~/store/planner-panel";
26
26
  import { T } from "~/ui/glyphs";
27
27
  import { TopBar, BottomBar } from "~/ui/Chrome";
28
28
  import { ModalLayer } from "~/ui/Modal";
@@ -62,16 +62,21 @@ process.on("SIGTERM", () => {
62
62
  // ≥ 150 col → all four zones
63
63
  // 120–149 → hide timeline
64
64
  // 100–119 → hide agents too
65
- // < 100 → hide virtual too (board is non-hideable)
65
+ // < 100 → hide planner too (board is non-hideable)
66
66
  //
67
- // User F1/F2/F3 toggles still work they last until the next resize, at
68
- // which point auto re-evaluates. Acceptable trade-off: resize events are
69
- // rare, predictable layout > sticky overrides.
67
+ // This only reports what FITS. The store combines it with each zone's enabled
68
+ // flag and the user's desired visibility, so F1/F2/F3 toggles persist across
69
+ // resizes and a disabled/hidden zone is never force-shown.
70
70
  function applyResponsiveLayout(): void {
71
71
  const width = process.stdout.columns ?? 200;
72
- store.setZoneVisible("timeline", width >= 150);
73
- store.setZoneVisible("agents", width >= 120);
74
- store.setZoneVisible("virtual", width >= 100);
72
+ // Report which zones FIT at this width. The store ANDs this with each zone's
73
+ // enabled flag and the user's desired visibility, so a disabled or
74
+ // intentionally-hidden zone is never force-shown just because there's room.
75
+ store.applyResponsiveFits({
76
+ planner: width >= 100,
77
+ timeline: width >= 150,
78
+ agents: width >= 120,
79
+ });
75
80
  }
76
81
  applyResponsiveLayout();
77
82
  process.stdout.on("resize", applyResponsiveLayout);
@@ -79,8 +84,8 @@ process.stdout.on("resize", applyResponsiveLayout);
79
84
  // Land on the Today/Tomorrow panel by default — for a daily-planning tool the
80
85
  // first question is "what's on my plate today", and that panel answers it. On
81
86
  // a narrow terminal where the panel auto-hides, fall back to the board.
82
- if (store.state.ui.visibleZones.virtual) {
83
- store.setActiveZone("virtual");
87
+ if (store.state.ui.visibleZones.planner) {
88
+ store.setActiveZone("planner");
84
89
  }
85
90
 
86
91
  const { view } = parseArgs(process.argv.slice(2));
@@ -97,11 +102,11 @@ function rootViewFor(v: ViewKind | undefined, s: TuiStore) {
97
102
  }
98
103
 
99
104
  function App() {
100
- const virtualItems = createMemo(() =>
101
- buildVirtualItems(store.state.boards.map((b) => b.board)),
105
+ const plannerItems = createMemo(() =>
106
+ buildPlannerItems(store.state.boards.map((b) => b.board)),
102
107
  );
103
108
 
104
- useKeyboard((key) => handleKey(store, key, virtualItems().length));
109
+ useKeyboard((key) => handleKey(store, key, plannerItems().length));
105
110
 
106
111
  return (
107
112
  <box
@@ -49,6 +49,30 @@ export interface Config {
49
49
  * Tokens are produced by `tuiboard calendar-setup`.
50
50
  */
51
51
  calendars?: CalendarsConfig;
52
+ /**
53
+ * Which optional zones are enabled and how they start. The board zone is
54
+ * always on (load-bearing) and not configurable here.
55
+ */
56
+ zones: ZonesConfig;
57
+ }
58
+
59
+ /**
60
+ * Per-zone startup mode:
61
+ * - "on" → enabled and visible at launch (default)
62
+ * - "off" → disabled entirely: never rendered, skipped by Shift-Tab, its
63
+ * F-key is inert, and its background work (calendar fetch /
64
+ * `~/.claude` watcher) never starts
65
+ * - "hidden" → enabled but collapsed at launch; reveal it with its F-key
66
+ */
67
+ export type ZoneMode = "on" | "off" | "hidden";
68
+
69
+ export interface ZonesConfig {
70
+ /** Today/Tomorrow cross-board panel. */
71
+ planner: ZoneMode;
72
+ /** 24h agenda + calendar overlay (internally the "timeline" zone). */
73
+ agenda: ZoneMode;
74
+ /** Live Claude Code session view. */
75
+ agents: ZoneMode;
52
76
  }
53
77
 
54
78
  export interface GoogleCalendarConfig {
@@ -79,6 +103,7 @@ export const DEFAULT_CONFIG: Omit<Config, "root" | "loaded" | "boards"> = {
79
103
  assignees: [],
80
104
  doneColumn: "Done",
81
105
  archiveColumn: "Archive",
106
+ zones: { planner: "on", agenda: "on", agents: "on" },
82
107
  };
83
108
 
84
109
  /**
@@ -135,6 +160,13 @@ interface RawConfig {
135
160
  color?: string;
136
161
  };
137
162
  };
163
+ // Values may be boolean (js-yaml parses on/off/yes/no → boolean) or the
164
+ // string "hidden"/"collapsed". Anything else falls back to "on".
165
+ zones: {
166
+ planner?: boolean | string;
167
+ agenda?: boolean | string;
168
+ agents?: boolean | string;
169
+ };
138
170
  }
139
171
 
140
172
  /** Expand `~` to the home dir; resolve relative paths against the config dir. */
@@ -173,6 +205,24 @@ function normalizeCalendars(
173
205
  return out.google || out.microsoft ? out : undefined;
174
206
  }
175
207
 
208
+ /** Normalize one zone's raw value to a ZoneMode. Default (undefined) → "on". */
209
+ function normalizeZoneMode(v: boolean | string | undefined): ZoneMode {
210
+ if (v === undefined || v === true) return "on";
211
+ if (v === false) return "off";
212
+ const s = String(v).toLowerCase();
213
+ if (s === "off" || s === "false" || s === "no" || s === "none") return "off";
214
+ if (s === "hidden" || s === "collapsed" || s === "closed") return "hidden";
215
+ return "on";
216
+ }
217
+
218
+ function normalizeZones(raw: RawConfig["zones"] | undefined): ZonesConfig {
219
+ return {
220
+ planner: normalizeZoneMode(raw?.planner),
221
+ agenda: normalizeZoneMode(raw?.agenda),
222
+ agents: normalizeZoneMode(raw?.agents),
223
+ };
224
+ }
225
+
176
226
  interface FoundConfig {
177
227
  path: string;
178
228
  dir: string;
@@ -239,6 +289,7 @@ function normalize(raw: Partial<RawConfig>, root: string, loaded: boolean): Conf
239
289
  ? raw.resume_command.map(String)
240
290
  : undefined,
241
291
  calendars: normalizeCalendars(raw.calendars, root),
292
+ zones: normalizeZones(raw.zones),
242
293
  };
243
294
  }
244
295
 
@@ -2,11 +2,11 @@
2
2
  * Centralized keyboard input handler. Dispatches based on:
3
3
  * 1. modal state (modal eats most keys)
4
4
  * 2. global keys (quit, help, undo, board switch, escape, zone cycle)
5
- * 3. active zone (virtual / board / timeline / agents)
5
+ * 3. active zone (planner / board / timeline / agents)
6
6
  *
7
7
  * Task-level actions (`t`/`m`/`s`/`b`/`a`/`o`/`Space`/`X`/`d`/`p`/`.`/`Enter`)
8
8
  * all route through `dispatchTaskAction`, so they work identically on a
9
- * cursor task whether you reach it via the kanban board, the virtual
9
+ * cursor task whether you reach it via the kanban board, the planner
10
10
  * panel, or a timeline block.
11
11
  *
12
12
  * Extracted from app.tsx so every root view (Dashboard, BoardOnly, etc.)
@@ -24,7 +24,7 @@ import {
24
24
  } from "~/store/index";
25
25
  import type { Board, PriorityLevel } from "~/types";
26
26
  import { buildTimelineEntries, formatAgendaDay } from "~/store/timeline";
27
- import { buildVirtualItems } from "~/store/virtual-panel";
27
+ import { buildPlannerItems } from "~/store/planner-panel";
28
28
  import { jumpToKanban } from "~/ui/TimelineView";
29
29
 
30
30
  interface KeyEvent {
@@ -37,7 +37,7 @@ interface KeyEvent {
37
37
  export function handleKey(
38
38
  store: TuiStore,
39
39
  key: KeyEvent,
40
- virtualCount: number,
40
+ plannerCount: number,
41
41
  ): void {
42
42
  const ui = store.state.ui;
43
43
  const board = store.state.boards[ui.activeBoardIndex]?.board;
@@ -125,17 +125,18 @@ export function handleKey(
125
125
  return;
126
126
  }
127
127
 
128
- // F1/F2/F3 toggle zone visibility. Board cannot be hidden.
128
+ // F1/F2/F3 toggle zone visibility. Board cannot be hidden; a disabled zone's
129
+ // key is inert (toggleZoneDesired no-ops).
129
130
  if (key.name === "f1") {
130
- store.setZoneVisible("virtual", !ui.visibleZones.virtual);
131
+ store.toggleZoneDesired("planner");
131
132
  return;
132
133
  }
133
134
  if (key.name === "f2") {
134
- store.setZoneVisible("timeline", !ui.visibleZones.timeline);
135
+ store.toggleZoneDesired("timeline");
135
136
  return;
136
137
  }
137
138
  if (key.name === "f3") {
138
- store.setZoneVisible("agents", !ui.visibleZones.agents);
139
+ store.toggleZoneDesired("agents");
139
140
  return;
140
141
  }
141
142
 
@@ -150,9 +151,9 @@ export function handleKey(
150
151
  return;
151
152
  }
152
153
 
153
- // Switch in/out of virtual panel with `v`
154
+ // Switch in/out of planner panel with `v`
154
155
  if (key.name === "v") {
155
- store.setActiveZone(ui.activeZone === "virtual" ? "board" : "virtual");
156
+ store.setActiveZone(ui.activeZone === "planner" ? "board" : "planner");
156
157
  return;
157
158
  }
158
159
 
@@ -183,7 +184,7 @@ export function handleKey(
183
184
  return;
184
185
  }
185
186
 
186
- // Zoom toggle: focus the active panel (board column or virtual panel)
187
+ // Zoom toggle: focus the active panel (board column or planner panel)
187
188
  // at full width.
188
189
  if (key.name === "z") {
189
190
  store.toggleZoom();
@@ -227,8 +228,8 @@ export function handleKey(
227
228
 
228
229
  // ─── Per-zone dispatching ───────────────────────────────────────────────
229
230
 
230
- if (ui.activeZone === "virtual") {
231
- handleVirtualZone(store, key, virtualCount, openLater);
231
+ if (ui.activeZone === "planner") {
232
+ handlePlannerZone(store, key, plannerCount, openLater);
232
233
  return;
233
234
  }
234
235
 
@@ -249,19 +250,19 @@ export function handleKey(
249
250
 
250
251
  // ─── Zone handlers ──────────────────────────────────────────────────────────
251
252
 
252
- function handleVirtualZone(
253
+ function handlePlannerZone(
253
254
  store: TuiStore,
254
255
  key: KeyEvent,
255
- virtualCount: number,
256
+ plannerCount: number,
256
257
  openLater: (m: ModalKind) => void,
257
258
  ): void {
258
259
  const ui = store.state.ui;
259
- const items = buildVirtualItems(store.state.boards.map((b) => b.board));
260
+ const items = buildPlannerItems(store.state.boards.map((b) => b.board));
260
261
  const target = items[ui.row];
261
262
 
262
263
  // Navigation
263
264
  if (key.name === "j" || key.name === "down") {
264
- store.setCursor(ui.col, Math.min(virtualCount - 1, ui.row + 1));
265
+ store.setCursor(ui.col, Math.min(plannerCount - 1, ui.row + 1));
265
266
  return;
266
267
  }
267
268
  if (key.name === "k" || key.name === "up") {
@@ -273,7 +274,7 @@ function handleVirtualZone(
273
274
  return;
274
275
  }
275
276
 
276
- // Task actions on the virtual cursor's target (works cross-board).
277
+ // Task actions on the planner cursor's target (works cross-board).
277
278
  if (target) {
278
279
  dispatchTaskAction(store, key, target.ref, openLater);
279
280
  }
@@ -501,7 +502,7 @@ function handleBoardZone(
501
502
  // unrendered, unscrollable column.
502
503
  const prev = adjacentVisibleColumn(store, board, ui.col, -1);
503
504
  if (prev === undefined) {
504
- store.setActiveZone("virtual");
505
+ store.setActiveZone("planner");
505
506
  } else {
506
507
  store.setCursor(prev, 0);
507
508
  }
@@ -551,7 +552,7 @@ function handleBoardZone(
551
552
 
552
553
  /**
553
554
  * Apply a task-level action to the given cursorRef. Used by every zone
554
- * (board / virtual / timeline) so the keys feel identical wherever the
555
+ * (board / planner / timeline) so the keys feel identical wherever the
555
556
  * cursor lives. Multi-select aware: when there are marked tasks, the
556
557
  * action operates on all of them via `applyToMarkedOr`.
557
558
  *
@@ -565,7 +566,7 @@ function dispatchTaskAction(
565
566
  ref: TaskRef,
566
567
  openLater: (m: ModalKind) => void,
567
568
  ): boolean {
568
- // Toggle done (Enter). Only meaningful for board/virtual; timeline has
569
+ // Toggle done (Enter). Only meaningful for board/planner; timeline has
569
570
  // its own Enter behavior (jump to kanban) which is handled earlier.
570
571
  if (key.name === "enter" || key.name === "return") {
571
572
  const n = store.applyToMarkedOr(ref, (r) => store.toggleDone(r));
@@ -623,7 +624,7 @@ function dispatchTaskAction(
623
624
  // Calendar arm mode (lowercase c): toggle a persistent mode for batch
624
625
  // scheduling onto the timeline. Entering also arms the cursor task and
625
626
  // focuses the timeline so you can immediately click a slot. While the mode
626
- // is on, clicking ANY task (board / virtual) arms it — click a task, click a
627
+ // is on, clicking ANY task (board / planner) arms it — click a task, click a
627
628
  // slot, repeat. `c` again or `Esc` exits. Works from any zone.
628
629
  if (key.name === "c" && !key.shift) {
629
630
  if (store.state.ui.armMode) {
@@ -10,7 +10,7 @@ describe("test runner smoke", () => {
10
10
  });
11
11
 
12
12
  /** Builds a minimal config with no boards — enough to exercise pure UI actions. */
13
- function emptyConfig(): Config {
13
+ function emptyConfig(overrides: Partial<Config> = {}): Config {
14
14
  return {
15
15
  root: process.cwd(),
16
16
  loaded: false,
@@ -18,6 +18,8 @@ function emptyConfig(): Config {
18
18
  assignees: [],
19
19
  doneColumn: "Done",
20
20
  archiveColumn: "Archive",
21
+ zones: { planner: "on", agenda: "on", agents: "on" },
22
+ ...overrides,
21
23
  };
22
24
  }
23
25
 
@@ -29,16 +31,16 @@ describe("UI activeZone", () => {
29
31
 
30
32
  it("setActiveZone updates the zone", () => {
31
33
  const store = createTuiStore({ config: emptyConfig() });
32
- store.setActiveZone("virtual");
33
- expect(store.state.ui.activeZone).toBe("virtual");
34
+ store.setActiveZone("planner");
35
+ expect(store.state.ui.activeZone).toBe("planner");
34
36
  store.setActiveZone("timeline");
35
37
  expect(store.state.ui.activeZone).toBe("timeline");
36
38
  });
37
39
 
38
- it("setActiveZone('virtual') resets row to 0", () => {
40
+ it("setActiveZone('planner') resets row to 0", () => {
39
41
  const store = createTuiStore({ config: emptyConfig() });
40
42
  store.setCursor(0, 7);
41
- store.setActiveZone("virtual");
43
+ store.setActiveZone("planner");
42
44
  expect(store.state.ui.row).toBe(0);
43
45
  });
44
46
  });
@@ -47,7 +49,7 @@ describe("UI visibleZones", () => {
47
49
  it("defaults to all four zones visible", () => {
48
50
  const store = createTuiStore({ config: emptyConfig() });
49
51
  expect(store.state.ui.visibleZones).toEqual({
50
- virtual: true,
52
+ planner: true,
51
53
  board: true,
52
54
  timeline: true,
53
55
  agents: true,
@@ -58,7 +60,7 @@ describe("UI visibleZones", () => {
58
60
  const store = createTuiStore({ config: emptyConfig() });
59
61
  store.setZoneVisible("timeline", false);
60
62
  expect(store.state.ui.visibleZones.timeline).toBe(false);
61
- expect(store.state.ui.visibleZones.virtual).toBe(true);
63
+ expect(store.state.ui.visibleZones.planner).toBe(true);
62
64
  expect(store.state.ui.visibleZones.board).toBe(true);
63
65
  expect(store.state.ui.visibleZones.agents).toBe(true);
64
66
  });
@@ -80,7 +82,7 @@ describe("UI visibleZones", () => {
80
82
  describe("UI cycleActiveZone", () => {
81
83
  it("cycles through all visible zones in fixed order", () => {
82
84
  const store = createTuiStore({ config: emptyConfig() });
83
- store.setActiveZone("virtual");
85
+ store.setActiveZone("planner");
84
86
  store.cycleActiveZone();
85
87
  expect(store.state.ui.activeZone).toBe("board");
86
88
  store.cycleActiveZone();
@@ -88,7 +90,7 @@ describe("UI cycleActiveZone", () => {
88
90
  store.cycleActiveZone();
89
91
  expect(store.state.ui.activeZone).toBe("agents");
90
92
  store.cycleActiveZone();
91
- expect(store.state.ui.activeZone).toBe("virtual"); // wrap
93
+ expect(store.state.ui.activeZone).toBe("planner"); // wrap
92
94
  });
93
95
 
94
96
  it("skips hidden zones", () => {
@@ -101,7 +103,7 @@ describe("UI cycleActiveZone", () => {
101
103
 
102
104
  it("is a no-op when only one zone is visible (board only)", () => {
103
105
  const store = createTuiStore({ config: emptyConfig() });
104
- store.setZoneVisible("virtual", false);
106
+ store.setZoneVisible("planner", false);
105
107
  store.setZoneVisible("timeline", false);
106
108
  store.setZoneVisible("agents", false);
107
109
  store.cycleActiveZone();
@@ -156,3 +158,67 @@ describe("Agenda day navigation", () => {
156
158
  expect(store.state.ui.row).toBe(0);
157
159
  });
158
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
+ });
@@ -8,7 +8,7 @@
8
8
  * The store also owns:
9
9
  * - The list of loaded boards (with their original mtime watermark)
10
10
  * - UI state: active board index, cursor (col/row), collapse flags,
11
- * filter, mode (kanban / virtual / list)
11
+ * filter, mode (kanban / planner / list)
12
12
  * - The undo log
13
13
  *
14
14
  * Mutations always:
@@ -71,17 +71,28 @@ export type ModalKind =
71
71
  | { kind: "help" };
72
72
 
73
73
  /** Which dashboard zone owns the keyboard cursor. */
74
- export type ActiveZone = "virtual" | "board" | "timeline" | "agents";
74
+ export type ActiveZone = "planner" | "board" | "timeline" | "agents";
75
75
 
76
76
  /** Fixed cycling order for Shift+Tab navigation. */
77
- const ZONE_ORDER: readonly ActiveZone[] = ["virtual", "board", "timeline", "agents"];
77
+ const ZONE_ORDER: readonly ActiveZone[] = ["planner", "board", "timeline", "agents"];
78
78
 
79
79
  export interface UIState {
80
80
  activeBoardIndex: number;
81
81
  /** Which dashboard zone owns the keyboard cursor right now. */
82
82
  activeZone: ActiveZone;
83
- /** Which zones are currently rendered. `board` cannot be hidden (load-bearing). */
83
+ /**
84
+ * Which zones are actually rendered right now. Derived from
85
+ * `enabledZones ∧ desiredVisible ∧ fits-at-current-width`. Read by the views;
86
+ * never set directly — go through setZoneVisible / toggleZoneDesired /
87
+ * applyResponsiveFits, which recompute it.
88
+ */
84
89
  visibleZones: Record<ActiveZone, boolean>;
90
+ /**
91
+ * Which zones are enabled at all (from the `zones:` config). A disabled zone
92
+ * is never rendered, is skipped by Shift-Tab, has an inert F-key, and its
93
+ * background work never started. `board` is always enabled.
94
+ */
95
+ enabledZones: Record<ActiveZone, boolean>;
85
96
  /** Column index (within active board) — meaningful only when `activeZone === "board"`. */
86
97
  col: number;
87
98
  /** Row index inside the active zone. */
@@ -109,7 +120,7 @@ export interface UIState {
109
120
  armedTimelineRef?: TaskRef;
110
121
  /**
111
122
  * Persistent calendar "arm mode" (toggled with `c`). While on, clicking any
112
- * task in the board / virtual panel arms it for the timeline, so you can
123
+ * task in the board / planner panel arms it for the timeline, so you can
113
124
  * schedule several tasks in a row — click a task, click a slot, repeat —
114
125
  * without re-pressing `c`. `Esc` (or `c` again) exits. Distinct from
115
126
  * `armedTimelineRef`, which is the single task currently armed.
@@ -150,7 +161,7 @@ export interface StoreState {
150
161
  undo: UndoEntry[];
151
162
  /**
152
163
  * Monotonic mutation counter. Bumped on every board mutation (via
153
- * `saveBoard`). Derived views (board columns, virtual panel, timeline) read
164
+ * `saveBoard`). Derived views (board columns, planner panel, timeline) read
154
165
  * it so their memos recompute on any change — a reliable top-level signal
155
166
  * dependency, since OpenTUI/Solid's fine-grained tracking of deeply-nested
156
167
  * store edits (e.g. growing a column's `children` array) doesn't always
@@ -169,12 +180,43 @@ export interface CreateStoreOptions {
169
180
  export function createTuiStore({ config }: CreateStoreOptions) {
170
181
  const initialBoards = loadAll(config);
171
182
 
183
+ // ─── Zone enablement (from `zones:` config) ──────────────────────────────
184
+ // `enabledZones` is a hard gate; `desiredVisible` is the user's runtime
185
+ // intent (F-keys flip it); `lastFits` is the terminal-width fit cache. The
186
+ // rendered `visibleZones` is the AND of all three. The board is always on.
187
+ const z = config.zones;
188
+ const enabledZones: Record<ActiveZone, boolean> = {
189
+ board: true,
190
+ planner: z.planner !== "off",
191
+ timeline: z.agenda !== "off",
192
+ agents: z.agents !== "off",
193
+ };
194
+ const desiredVisible: Record<ActiveZone, boolean> = {
195
+ board: true,
196
+ planner: z.planner === "on",
197
+ timeline: z.agenda === "on",
198
+ agents: z.agents === "on",
199
+ };
200
+ let lastFits: Record<ActiveZone, boolean> = {
201
+ board: true,
202
+ planner: true,
203
+ timeline: true,
204
+ agents: true,
205
+ };
206
+ const computeVisible = (): Record<ActiveZone, boolean> => ({
207
+ board: true,
208
+ planner: enabledZones.planner && desiredVisible.planner && lastFits.planner,
209
+ timeline: enabledZones.timeline && desiredVisible.timeline && lastFits.timeline,
210
+ agents: enabledZones.agents && desiredVisible.agents && lastFits.agents,
211
+ });
212
+
172
213
  const [state, setState] = createStore<StoreState>({
173
214
  boards: initialBoards,
174
215
  ui: {
175
216
  activeBoardIndex: 0,
176
217
  activeZone: "board",
177
- visibleZones: { virtual: true, board: true, timeline: true, agents: true },
218
+ visibleZones: computeVisible(),
219
+ enabledZones,
178
220
  col: 0,
179
221
  row: 0,
180
222
  zoomed: false,
@@ -198,11 +240,17 @@ export function createTuiStore({ config }: CreateStoreOptions) {
198
240
  );
199
241
 
200
242
  // Agents store has its own lifecycle (chokidar watcher on ~/.claude).
201
- // Shared dispose() boundary below so SIGINT cleans both.
202
- const agentsStore: AgentsStore = createAgentsStore();
203
- // Calendar feeds (read-only) merged into the Agenda zone. No-op when no
204
- // `calendars:` block is configured.
205
- const calendarStore: CalendarStore = createCalendarStore(config.calendars, isoToday);
243
+ // Shared dispose() boundary below so SIGINT cleans both. When the agents
244
+ // zone is disabled we skip the watcher entirely (no `~/.claude` reads at all)
245
+ // and hand back an inert stub.
246
+ const agentsStore: AgentsStore = enabledZones.agents
247
+ ? createAgentsStore()
248
+ : noopAgentsStore();
249
+ // Calendar feeds (read-only) merged into the Agenda zone. Skipped entirely
250
+ // (no network) when the agenda zone is disabled.
251
+ const calendarStore: CalendarStore = enabledZones.timeline
252
+ ? createCalendarStore(config.calendars, isoToday)
253
+ : noopCalendarStore();
206
254
  watcher.onChange((filepath) => {
207
255
  // External edit. Re-read this board from disk.
208
256
  try {
@@ -716,19 +764,46 @@ export function createTuiStore({ config }: CreateStoreOptions) {
716
764
 
717
765
  function setActiveZone(zone: ActiveZone): void {
718
766
  setState("ui", "activeZone", zone);
719
- // Moving to a vertical-list zone (virtual / agents / timeline) resets
767
+ // Moving to a vertical-list zone (planner / agents / timeline) resets
720
768
  // the row cursor to the top so the user always lands somewhere sensible.
721
769
  if (zone !== "board") setState("ui", "row", 0);
722
770
  }
723
771
 
772
+ /** Recompute `visibleZones` from enabled ∧ desired ∧ fits; bounce the cursor
773
+ * to the board if the active zone just became invisible. */
774
+ function recomputeVisible(): void {
775
+ const v = computeVisible();
776
+ setState("ui", "visibleZones", v);
777
+ if (!v[state.ui.activeZone]) setActiveZone("board");
778
+ }
779
+
780
+ /** Set a zone's desired visibility (user intent). Showing a disabled zone is
781
+ * ignored; the board can never be hidden. Used by direct reveals (day-nav,
782
+ * arm flow) and the responsive layout's per-zone calls. */
724
783
  function setZoneVisible(zone: ActiveZone, visible: boolean): void {
725
- // Board is the load-bearing zone — never allow it to be hidden.
726
784
  if (zone === "board" && !visible) return;
727
- setState("ui", "visibleZones", zone, visible);
728
- // If we just hid the active zone, bounce the cursor to "board".
729
- if (!visible && state.ui.activeZone === zone) {
730
- setActiveZone("board");
731
- }
785
+ if (visible && !enabledZones[zone]) return; // can't reveal a disabled zone
786
+ desiredVisible[zone] = visible;
787
+ recomputeVisible();
788
+ }
789
+
790
+ /** Flip a zone's visibility (the F1/F2/F3 toggles). No-op if disabled. */
791
+ function toggleZoneDesired(zone: ActiveZone): void {
792
+ if (zone === "board" || !enabledZones[zone]) return;
793
+ desiredVisible[zone] = !desiredVisible[zone];
794
+ recomputeVisible();
795
+ }
796
+
797
+ /** Update the terminal-width fit cache (called by the responsive layout) and
798
+ * recompute. Never overrides enablement or the user's desired visibility. */
799
+ function applyResponsiveFits(fits: Partial<Record<ActiveZone, boolean>>): void {
800
+ lastFits = {
801
+ board: true,
802
+ planner: fits.planner !== false,
803
+ timeline: fits.timeline !== false,
804
+ agents: fits.agents !== false,
805
+ };
806
+ recomputeVisible();
732
807
  }
733
808
 
734
809
  function cycleActiveZone(): void {
@@ -1034,6 +1109,8 @@ export function createTuiStore({ config }: CreateStoreOptions) {
1034
1109
  setCursor,
1035
1110
  setActiveZone,
1036
1111
  setZoneVisible,
1112
+ toggleZoneDesired,
1113
+ applyResponsiveFits,
1037
1114
  cycleActiveZone,
1038
1115
  toggleZoom,
1039
1116
  toggleGrab,
@@ -1069,6 +1146,22 @@ export type TuiStore = ReturnType<typeof createTuiStore>;
1069
1146
 
1070
1147
  // ─── Load helpers ────────────────────────────────────────────────────────────
1071
1148
 
1149
+ /** Inert agents store for when the agents zone is disabled — no chokidar
1150
+ * watcher, no `~/.claude` reads at all. */
1151
+ function noopAgentsStore(): AgentsStore {
1152
+ return { sessions: () => [], refresh: () => {}, dispose: async () => {} };
1153
+ }
1154
+
1155
+ /** Inert calendar store for when the agenda zone is disabled — no fetching. */
1156
+ function noopCalendarStore(): CalendarStore {
1157
+ return {
1158
+ events: () => [],
1159
+ setActiveDate: () => {},
1160
+ refresh: () => {},
1161
+ dispose: async () => {},
1162
+ };
1163
+ }
1164
+
1072
1165
  function loadAll(config: Config): LoadedBoard[] {
1073
1166
  const out: LoadedBoard[] = [];
1074
1167
  for (const b of config.boards) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Compact agent status strip for the dashboard. The non-archived sessions
3
3
  * render inside a vertical scrollbox so the list scrolls with the mouse wheel
4
- * (like the board / virtual / timeline zones) AND follows the keyboard cursor
4
+ * (like the board / planner / timeline zones) AND follows the keyboard cursor
5
5
  * via scrollChildIntoView — same pattern as BoardView's columns.
6
6
  *
7
7
  * The border color reflects activeZone === "agents" so the cursor ring is
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Kanban board view — horizontal scrolling row of full-height columns.
3
3
  *
4
- * Mirrors the Today/Tomorrow virtual panel: each column has a fixed width
4
+ * Mirrors the Today/Tomorrow planner panel: each column has a fixed width
5
5
  * and stretches vertically to fill the board area. Columns that don't fit
6
6
  * the available horizontal space are reached via horizontal scroll. Zoom
7
7
  * mode collapses everything down to the single active column at full
@@ -88,7 +88,7 @@ export function BoardView(props: BoardViewProps) {
88
88
  * (Python kanban `z`).
89
89
  */
90
90
  const renderedColumns = createMemo(() => {
91
- if (!ui().zoomed || ui().activeZone === "virtual") return visibleColumns();
91
+ if (!ui().zoomed || ui().activeZone === "planner") return visibleColumns();
92
92
  const cols = visibleColumns();
93
93
  // ui.col is a board.columns index (carries Archive); map it to the
94
94
  // rendered list so zoom focuses the column actually under the cursor.
@@ -111,7 +111,7 @@ export function BoardView(props: BoardViewProps) {
111
111
  // geometry used everywhere (COL_WIDTH + COL_GAP).
112
112
  createEffect(() => {
113
113
  const colIdx = ui().col;
114
- if (ui().zoomed || ui().activeZone === "virtual") {
114
+ if (ui().zoomed || ui().activeZone === "planner") {
115
115
  setScrollX(0);
116
116
  return;
117
117
  }
@@ -153,7 +153,7 @@ export function BoardView(props: BoardViewProps) {
153
153
  };
154
154
 
155
155
  // Measure the viewport width once at mount, regardless of the active zone.
156
- // Otherwise — since tuiboard now starts focused on the virtual panel — the
156
+ // Otherwise — since tuiboard now starts focused on the planner panel — the
157
157
  // scroll effect early-returns and viewportW stays 0, so partly-clipped
158
158
  // columns briefly show their task rows until the board is first touched.
159
159
  onMount(() => {
@@ -328,7 +328,7 @@ function ColumnView(props: ColumnViewProps) {
328
328
  flexShrink: 0,
329
329
  // No explicit height — Yoga stretches us along the row's cross
330
330
  // axis, so the column always fills the full board height. Same
331
- // contract as the Today/Tomorrow virtual panel next door.
331
+ // contract as the Today/Tomorrow planner panel next door.
332
332
  marginRight: COL_GAP,
333
333
  border: true,
334
334
  borderStyle: "rounded",
package/src/ui/Modal.tsx CHANGED
@@ -588,12 +588,12 @@ function HelpModal(props: { store: TuiStore }) {
588
588
  <span style={{ fg: T.text }}>{" h j k l ←↑↓→ Move cursor inside the active zone\n"}</span>
589
589
  <span style={{ fg: T.text }}>{" Tab Next board (kanban zone)\n"}</span>
590
590
  <span style={{ fg: T.text }}>{" 1..9 Jump to board N\n"}</span>
591
- <span style={{ fg: T.text }}>{" v Toggle Today/Tomorrow virtual panel focus\n"}</span>
592
- <span style={{ fg: T.text }}>{" Shift-Tab Cycle active zone (virtual → board → timeline → agents)\n"}</span>
593
- <span style={{ fg: T.text }}>{" F1 / F2 / F3 Toggle visibility of Virtual / Timeline / Agents zones\n"}</span>
591
+ <span style={{ fg: T.text }}>{" v Toggle Today/Tomorrow planner panel focus\n"}</span>
592
+ <span style={{ fg: T.text }}>{" Shift-Tab Cycle active zone (planner → board → timeline → agents)\n"}</span>
593
+ <span style={{ fg: T.text }}>{" F1 / F2 / F3 Toggle visibility of Planner / Timeline / Agents zones\n"}</span>
594
594
  <span style={{ fg: T.text }}>{" z Zoom active zone (or column) to full screen\n"}</span>
595
595
  <span style={{ fg: T.text }}>{" r Refresh everything (boards from disk, agents, agenda calendar)\n"}</span>
596
- <span style={{ fg: T.textDim }}>{"\nTask actions (work in board, virtual, AND timeline zones)\n"}</span>
596
+ <span style={{ fg: T.textDim }}>{"\nTask actions (work in board, planner, AND timeline zones)\n"}</span>
597
597
  <span style={{ fg: T.text }}>{" Enter Toggle done\n"}</span>
598
598
  <span style={{ fg: T.text }}>{" o Open detail view\n"}</span>
599
599
  <span style={{ fg: T.text }}>{" e Edit task text\n"}</span>
@@ -1,14 +1,14 @@
1
- /** The fixed Today/Tomorrow virtual panel — always on the left. */
1
+ /** The fixed Today/Tomorrow planner panel — always on the left. */
2
2
 
3
3
  import { For, Show, createEffect, createMemo } from "solid-js";
4
4
 
5
5
  import { ATTR, T } from "~/ui/glyphs";
6
6
  import { TaskRow } from "~/ui/TaskRow";
7
7
  import {
8
- buildVirtualItems,
9
- groupVirtualItems,
10
- type VirtualGroup,
11
- } from "~/store/virtual-panel";
8
+ buildPlannerItems,
9
+ groupPlannerItems,
10
+ type PlannerGroup,
11
+ } from "~/store/planner-panel";
12
12
  import type { TuiStore } from "~/store/index";
13
13
 
14
14
  interface ScrollBoxLike {
@@ -32,15 +32,15 @@ const BUCKET_HEADER: Record<string, { label: string; color: string }> = {
32
32
  priority: { label: "🔺 Priority", color: T.today },
33
33
  };
34
34
 
35
- export function VirtualPanel(props: { store: TuiStore }) {
35
+ export function PlannerPanel(props: { store: TuiStore }) {
36
36
  const items = createMemo(() => {
37
37
  props.store.state.rev; // recompute on any board mutation
38
- return buildVirtualItems(props.store.state.boards.map((b) => b.board));
38
+ return buildPlannerItems(props.store.state.boards.map((b) => b.board));
39
39
  });
40
- const groups = createMemo(() => groupVirtualItems(items()));
41
- const isActive = createMemo(() => props.store.state.ui.activeZone === "virtual");
40
+ const groups = createMemo(() => groupPlannerItems(items()));
41
+ const isActive = createMemo(() => props.store.state.ui.activeZone === "planner");
42
42
  const isZoomed = createMemo(
43
- () => props.store.state.ui.zoomed && props.store.state.ui.activeZone === "virtual",
43
+ () => props.store.state.ui.zoomed && props.store.state.ui.activeZone === "planner",
44
44
  );
45
45
  const cursorRow = createMemo(() => props.store.state.ui.row);
46
46
  let scrollBoxRef: ScrollBoxLike | undefined;
@@ -121,7 +121,7 @@ export function VirtualPanel(props: { store: TuiStore }) {
121
121
  availableWidth={isZoomed() ? 100 : 36}
122
122
  isMarkedFn={(r) => props.store.isMarked(r)}
123
123
  onClickItem={(flatIndex) => {
124
- props.store.setActiveZone("virtual");
124
+ props.store.setActiveZone("planner");
125
125
  props.store.setCursor(0, flatIndex);
126
126
  // In calendar arm mode, a click also arms the task for the
127
127
  // timeline (click a task, then a slot to place it).
@@ -141,7 +141,7 @@ export function VirtualPanel(props: { store: TuiStore }) {
141
141
  }
142
142
 
143
143
  function RenderGroups(props: {
144
- groups: VirtualGroup[];
144
+ groups: PlannerGroup[];
145
145
  isActive: boolean;
146
146
  cursorRow: number;
147
147
  availableWidth: number;
@@ -258,7 +258,7 @@ function RenderGroups(props: {
258
258
  }
259
259
 
260
260
  function isFirstOfSection(
261
- groups: VirtualGroup[],
261
+ groups: PlannerGroup[],
262
262
  idx: number,
263
263
  ): boolean {
264
264
  if (idx === 0) return true;
@@ -354,7 +354,7 @@ export function TimelineView(props: TimelineViewProps) {
354
354
  </Show>
355
355
 
356
356
  {/* The 24h grid now owns the whole panel — no sticky section above
357
- it. Tasks are armed for scheduling from the board / virtual panel
357
+ it. Tasks are armed for scheduling from the board / planner panel
358
358
  via the `C` shortcut, then placed by clicking a slot here. */}
359
359
  <scrollbox
360
360
  ref={(r: ScrollBoxLike) => (scrollBoxRef = r)}
package/src/ui/glyphs.ts CHANGED
@@ -29,7 +29,7 @@ export const ATTR = {
29
29
  * instead of raw ANSI names like "red"/"yellow" — those tend to render
30
30
  * as fully-saturated traffic-light colors in most themes and clash
31
31
  * with everything around them.
32
- * - The Today/Tomorrow virtual panel has its own *warm* identity
32
+ * - The Today/Tomorrow planner panel has its own *warm* identity
33
33
  * (peach/orange) so the user instantly knows "this is the time zone";
34
34
  * everything else uses a *cool* identity (soft cyan) for active focus.
35
35
  * - Cursor row highlight is the only opaque paint outside the modal.
@@ -137,7 +137,7 @@ export function cellWidth(s: string): number {
137
137
  }
138
138
 
139
139
  /**
140
- * Per-board accent palette. Used by the virtual panel to color-code the
140
+ * Per-board accent palette. Used by the planner panel to color-code the
141
141
  * source-board tag on priority/agenda items, so the user can recognize
142
142
  * which board a cross-cutting item came from at a glance.
143
143
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Standalone fullscreen view for the kanban board (with virtual panel).
2
+ * Standalone fullscreen view for the kanban board (with planner panel).
3
3
  * Mounted when the user launches `tuiboard --view=board`, AND used inside
4
4
  * the dashboard via composition.
5
5
  *
@@ -13,7 +13,7 @@
13
13
  import { Show, createMemo } from "solid-js";
14
14
 
15
15
  import { BoardView } from "~/ui/BoardView";
16
- import { VirtualPanel } from "~/ui/VirtualPanel";
16
+ import { PlannerPanel } from "~/ui/PlannerPanel";
17
17
  import type { TuiStore } from "~/store/index";
18
18
 
19
19
  export function BoardOnly(props: { store: TuiStore }) {
@@ -24,10 +24,10 @@ export function BoardOnly(props: { store: TuiStore }) {
24
24
 
25
25
  return (
26
26
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
27
- <Show when={!ui().zoomed || ui().activeZone === "virtual"}>
28
- <VirtualPanel store={props.store} />
27
+ <Show when={!ui().zoomed || ui().activeZone === "planner"}>
28
+ <PlannerPanel store={props.store} />
29
29
  </Show>
30
- <Show when={(!ui().zoomed || ui().activeZone !== "virtual") && activeBoard()}>
30
+ <Show when={(!ui().zoomed || ui().activeZone !== "planner") && activeBoard()}>
31
31
  <BoardView store={props.store} board={activeBoard()!} />
32
32
  </Show>
33
33
  </box>
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The default tuiboard view: a 4-zone dashboard.
3
3
  *
4
- * ┌─Virtual─┬─Board────────┬─Timeline─┐
4
+ * ┌─Planner─┬─Board────────┬─Timeline─┐
5
5
  * │ │ │ │
6
6
  * │ │ │ │
7
7
  * │ ├──────────────┤ │
@@ -24,7 +24,7 @@ import { Show, createMemo } from "solid-js";
24
24
  import { AgentsBar } from "~/ui/AgentsBar";
25
25
  import { BoardView } from "~/ui/BoardView";
26
26
  import { TimelineView } from "~/ui/TimelineView";
27
- import { VirtualPanel } from "~/ui/VirtualPanel";
27
+ import { PlannerPanel } from "~/ui/PlannerPanel";
28
28
  import { AgentsOnly } from "~/views/AgentsOnly";
29
29
  import { BoardOnly } from "~/views/BoardOnly";
30
30
  import { TimelineOnly } from "~/views/TimelineOnly";
@@ -53,7 +53,7 @@ export function Dashboard(props: { store: TuiStore }) {
53
53
  * standalone view wrappers so a `z` press in the dashboard produces
54
54
  * the same visual as launching `tuiboard --view=<zone>`.
55
55
  *
56
- * Board + virtual share BoardOnly because both live in the top-left
56
+ * Board + planner share BoardOnly because both live in the top-left
57
57
  * zone of the normal layout and BoardOnly already respects ui.zoomed
58
58
  * to render only the active panel between them.
59
59
  */
@@ -80,12 +80,12 @@ function FourZoneLayout(props: { store: TuiStore }) {
80
80
 
81
81
  return (
82
82
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
83
- {/* Left column: virtual + board on top, agents on bottom */}
83
+ {/* Left column: planner + board on top, agents on bottom */}
84
84
  <box style={{ flexDirection: "column", flexGrow: 1 }}>
85
85
  {/* Top zone */}
86
86
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
87
- <Show when={visible().virtual}>
88
- <VirtualPanel store={props.store} />
87
+ <Show when={visible().planner}>
88
+ <PlannerPanel store={props.store} />
89
89
  </Show>
90
90
  <Show when={visible().board && activeBoard()}>
91
91
  <BoardView store={props.store} board={activeBoard()!} />