Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [3.9.0] - 2026-06-17

### Added

- **Edit a session while it runs — `[e]`.** During a live session, **`[e]`** opens
the session form pre-filled with the current goal, details, focus target and
break budget. Adjust anything and press **`Enter`** — the changes apply **without
stopping the timer**, and the crash-recovery journal is re-seeded immediately so
the new metadata survives an interruption. **`Esc`** discards the edit. This is
the "just start the clock, decide what I'm doing mid-flow" workflow. The form is
the existing new-session form reused in an `"edit"` mode.

### Changed

- **Break-category picker is now `[c]` (was `[m]` "more").** The label "more"
carried no meaning; **`[c]`** ("categories") opens the picker in both states —
off break, picking a category starts a break in it; on break, it switches the
current category. The old `[c]` cycle-through-categories binding is removed (the
picker supersedes it). Quick keys **`1`–`6`** are unchanged.
- **Centered, compact session footer.** The live control row is centered and
trimmed to the essentials (`[p] pause · [b] break · [c] cat · [e] edit ·
[x] cancel · [q] stop`) so it actually fits and centers in a small terminal —
the full keymap stays in **`[6]` Help**. `z` (zen), `Enter` (hide), `Tab`
(views), `r` (reset) and `1`–`6` all still work; they're just no longer crowding
the footer. All footer rows are now centered.
- **Header reads "Flowclock" (dropped "Dashboard").** It's already an interactive
dashboard; the second word was redundant.

## [3.8.0] - 2026-06-15

### Added
Expand Down
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ The big 7-segment counter is scaled "reserve-first" — it gets ample vertical
space before the surrounding metadata, so it never appears cramped:

```
Flowclock Dashboard 12:23:28 [1:Session] 2:Overview 3:Sessions 4:Goals 5:Breaks 6:Help
Flowclock 12:23:28 [1:Session] 2:Overview 3:Sessions 4:Goals 5:Breaks 6:Help
╔══════════════════════════════════════════════════════════╗
║ Deep work — StreamNet ║
║ ║
Expand All @@ -199,25 +199,31 @@ space before the surrounding metadata, so it never appears cramped:
║ ████ ███ ████ ████ █ ████ ║
║ ║
║ goal · 42m/1h ███████░░░ 70% break 06:00/20:00 · ratio 1:7.0 ║
║ [p] pause [b] break [1-6] category [r] reset [q] stop & save ║
╚══════════════════════════════════════════════════════════╝
[p] pause · [b] break · [c] cat · [e] edit · [x] cancel · [q] stop
```

The control row lives in the **centered footer** below the panel (trimmed to the
essentials so it fits and centers in a small terminal — `[6] Help` lists them all).

Live session controls inside the dashboard:

| Key | Action |
| --- | ------ |
| `p` | Pause / resume |
| `b` | Start / end a break |
| `1`–`6` | Pick a quick break category (rest · meal · exercise · walk · distraction · other) |
| `m` | Open the break-category picker — **all** categories, incl. **coffee** and **sleep** |
| `c` | Open the break-category **picker** — **all** categories, incl. **coffee** and **sleep** |
| `e` | **Edit** the running session — goal, details, focus target, break budget (timer keeps counting) |
| `x` | **Cancel** the session (discard without saving — confirmed first) |
| `r` | Reset the session clock |
| `z` | Zen — hide metadata, big clock only |
| `Enter` | Hide / show the control footer |
| `q` | Stop & **save** the session (a summary shows, then the Session view) |

`q` saves, `x` discards. The full category set is `rest · meal · exercise ·
walk · distraction · other · coffee · sleep`; keys `1`–`6` stay bound to the
first six so the footer stays short, and `[m]` reaches the rest.
first six so the footer stays short, and **`[c]`** opens a picker for the rest.

### Starting a session — the in-dashboard form

Expand Down Expand Up @@ -252,7 +258,17 @@ flowclock start --goal "Deep work — StreamNet" --target 1h --break-budget 20m
Every text field is a real editor: move with **← →**, **Home**/**End**, delete
around the cursor with **Backspace**/**Del**, and **paste** clipboard text
(multi-line pastes are flattened to one line). The same applies to the edit
overlay (`[e]` on the Sessions view).
overlays — `[e]` on the Sessions view (logged sessions) and `[e]` during a live
session (see below).

### Editing a running session — `[e]`

Sometimes you just want to **start the clock** before you've decided exactly what
you're doing. Begin a plain session, then press **`e`** at any point to open the
same form, pre-filled with the current goal, details, focus target and break
budget. Adjust anything, press **`Enter`**, and the changes apply **without
stopping the timer** — the session keeps counting and the crash-recovery journal
picks up the new metadata immediately. **`Esc`** discards the edit.

### Cancelling a session — `[x]` (discard) vs `[q]` (save)

Expand Down Expand Up @@ -357,7 +373,7 @@ flowclock dashboard --json # emits DashboardSnapshot, never launches the TUI
```

```
Flowclock Dashboard 12:23:28 [1:Overview] 2:Sessions 3:Goals 4:Breaks
Flowclock 12:23:28 [1:Overview] 2:Sessions 3:Goals 4:Breaks
┌─ Today ───────────────────────┐ ┌─ Flow ──────────────────────────────┐
│ Focus 1h 45m 2 sessions │ │ Flow score ████████░░░░ 58/100 │
│ Break 12m ratio 8.8:1│ │ Daily goal ████░░░░░░░░ 44% (4h) │
Expand Down Expand Up @@ -491,7 +507,8 @@ on-disk schema is **v3**; migrations are non-destructive.
| **v3.5.1** ✅ | `classic` lightened to `░` shade so block `█` / classic `░` / bold `▓` read as three distinct surfaces while keeping exact 5-row footprint parity |
| **v3.6.0** ✅ | `classic`/`bold` redesigned as native 5-row fonts with distinct glyph shapes (cornered `classic` / heavy-slab `bold`, both solid) — replaces the v3.5.x shade-fill approach so the three solid styles read as clearly distinct while keeping exact footprint parity |
| **v3.7.0** ✅ | Session editing — in-dashboard `[e]` overlay (view 3 · Sessions) and `flowclock edit <id>` CLI; editable fields: focus, break total, goal, details; start immutable; end + break timeline recompute automatically |
| **v3.8.0** ✅ | Crash recovery (resume an interrupted session); `[x]` cancel-without-saving; `coffee` + `sleep` break categories with an `[m]` picker; full line editors (cursor + paste) in every text field; "Name" → "Details" (CLI `--details`, `--name`/`--label` kept as aliases) |
| **v3.8.0** ✅ | Crash recovery (resume an interrupted session); `[x]` cancel-without-saving; `coffee` + `sleep` break categories with a picker; full line editors (cursor + paste) in every text field; "Name" → "Details" (CLI `--details`, `--name`/`--label` kept as aliases) |
| **v3.9.0** ✅ | Edit a **running** session with `[e]` (goal/details/target/budget, timer keeps counting); break-category picker moved to **`[c]`** (was `[m] more`); centered, compact session footer; header reads **Flowclock** (dropped the "Dashboard" suffix) |
| **next** | `flowclock sync` — push `sessions.json` to a self-hosted/cloud endpoint; recurring goals; dashboard filters |
| **later** | Per-goal analytics deep-dives, calendar heatmap, Homebrew tap |

Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flowclock-cli",
"version": "3.8.0",
"version": "3.9.0",
"description": "Flowtime count-up terminal timer (HUD, not Pomodoro) with silent session logging — agent-native, like gh and vercel.",
"type": "module",
"license": "AGPL-3.0-or-later",
Expand Down Expand Up @@ -64,6 +64,9 @@
"env-paths": "^3.0.0",
"zod": "^3.25.0"
},
"overrides": {
"hono": ">=4.12.25"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
"@types/node": "^20.14.0",
Expand Down
112 changes: 84 additions & 28 deletions src/tui/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import type { CommandContext } from "../lib/context.js";
import type { Session } from "../schemas/session.js";
import type { ThemeName, DisplayStyle } from "../schemas/config.js";
import { ALL_BREAK_CATEGORIES, QUICK_BREAK_CATEGORIES } from "../schemas/session.js";
import { QUICK_BREAK_CATEGORIES } from "../schemas/session.js";
import type { BreakCategory } from "../schemas/session.js";
import { buildSnapshot } from "../lib/snapshot.js";
import { readSessions, appendSession, deleteSession, updateSession } from "../lib/session.js";
Expand Down Expand Up @@ -100,7 +100,7 @@ const VIEW_LABELS: Record<ViewName, string> = {
};

/** Ordered break categories — digit keys 1..6 map to this array. The full set
* (incl. coffee/sleep) is reachable via the break-category picker ([m]). */
* (incl. coffee/sleep) is reachable via the break-category picker ([c]). */
const BREAK_CATEGORIES: readonly BreakCategory[] = QUICK_BREAK_CATEGORIES;

/** All available themes in cycle order. */
Expand Down Expand Up @@ -144,7 +144,7 @@ interface AppState {
form: SessionFormState;
edit: EditFormState;
confirm: ConfirmState;
/** Break-category picker overlay ([m] during a live session). */
/** Break-category picker overlay ([c] during a live session). */
breakpicker: BreakPickerState;
/** Resume-previous-session overlay (shown at launch after a crash). */
resume: ResumeState;
Expand Down Expand Up @@ -263,8 +263,8 @@ export function buildFrame(
: ` ${VIEW_LABELS[v]} `,
).join(" ");
const title = color
? paint("Flowclock Dashboard", theme, true)
: "Flowclock Dashboard";
? paint("Flowclock", theme, true)
: "Flowclock";

// Live indicator appended to headerRight when a session is running
let headerRight: string;
Expand Down Expand Up @@ -382,7 +382,8 @@ export function buildFrame(
} else if (state.breakpicker.open) {
footerHints = "[↑↓] select · [1-8] pick · [Enter] choose · [Esc] cancel";
} else if (state.form.open) {
footerHints = "[Tab] next field · [Enter] start · [Esc] cancel";
const verb = state.form.mode === "edit" ? "save" : "start";
footerHints = `[Tab] next field · [Enter] ${verb} · [Esc] cancel`;
} else if (state.edit.open) {
footerHints = "[Tab] next field · [Enter] save · [Esc] cancel";
} else if (state.palette.open) {
Expand All @@ -393,10 +394,11 @@ export function buildFrame(
footerHints = "";
} else {
const kb = ctx.config.keybindings;
const cat = kb.category ?? "c";
if (state.live.timer.isOnBreak) {
footerHints = `[1-6] cat · [m] more · [${kb.break}] resume · [Enter] hide`;
footerHints = `[${cat}] cat · [${kb.break}] resume · [e] edit · [Enter] hide`;
} else {
footerHints = `[${kb.pause}] pause · [${kb.break}] break · [1-6]/[m] cat · [x] cancel · [z] zen · [Enter] hide · [${kb.quit}] stop · [Tab] views`;
footerHints = `[${kb.pause}] pause · [${kb.break}] break · [${cat}] cat · [e] edit · [x] cancel · [${kb.quit}] stop`;
}
}
} else if (state.live) {
Expand All @@ -406,7 +408,7 @@ export function buildFrame(
} else {
footerHints = "[Tab] views · [s] start · [/] commands · [d] style · [t] theme · [q] quit";
}
const footerLine = padTo(footerHints, cols);
const footerLine = padTo(footerHints, cols, "center");
if (footerRect) frame.push(footerLine);

// Ensure exactly `rows` lines (pad if needed)
Expand Down Expand Up @@ -849,6 +851,57 @@ export async function runDashboardApp(
render();
}

/**
* Open the session form in EDIT mode over the RUNNING session, pre-filled
* with its current goal/details and target/budget (compact, parser-friendly).
* Lets you start a session "blind" and fill in what you're doing mid-flow.
*/
function openLiveEditForm() {
if (!state.live) return;
const live = state.live;
state.form = openSessionFormState({
mode: "edit",
values: {
goal: live.goal ?? "",
label: live.label ?? "",
target: live.focusTargetS != null ? compactDuration(live.focusTargetS) : "",
break: live.breakBudgetS != null ? compactDuration(live.breakBudgetS) : "",
},
});
state.view = "session";
render();
}

/**
* Apply the form's values to the LIVE session (goal/details/target/budget).
* The timer keeps running; on a duration parse error, surface it and stay
* open. Re-seeds the crash-recovery journal so the new metadata survives.
*/
function applyLiveEdit(values: SessionFormValues) {
if (!state.live) return;
let focusTargetS: number | null = null;
let breakBudgetS: number | null = null;
try {
if (values.target.trim()) focusTargetS = parseDurationToS(values.target);
if (values.break.trim()) breakBudgetS = parseDurationToS(values.break);
} catch (err) {
state.form = { ...state.form, error: (err as Error).message };
render();
return;
}
state.live = {
...state.live,
goal: values.goal.trim() || null,
label: values.label.trim() || null,
focusTargetS,
breakBudgetS,
};
state.form = emptySessionFormState();
state.view = "session";
writeLiveJournal();
render();
}

/**
* Open the edit form for the currently-selected session, pre-filled with its
* goal/name and its focus/break totals in a parser-friendly compact form.
Expand Down Expand Up @@ -1049,19 +1102,27 @@ export async function runDashboardApp(

// ── (1b) New-session form: feed keys to its reducer ───────────────────
if (state.form.open) {
// Ctrl-C aborts the form and exits cleanly (no live session yet).
// Ctrl-C: create mode has no session yet → exit cleanly; edit mode has a
// session running → just close the form and leave it counting.
if (key.name === "char" && key.char === CTRL_C) {
cleanup();
if (state.form.mode === "edit") {
state.form = emptySessionFormState();
render();
} else {
cleanup();
}
return;
}
const mode = state.form.mode;
const result = sessionFormApplyKey(state.form, key);
state.form = result.state;
if (result.action?.type === "cancel") {
render();
return;
}
if (result.action?.type === "submit") {
startSessionFromForm(result.action.values);
if (mode === "edit") applyLiveEdit(result.action.values);
else startSessionFromForm(result.action.values);
return;
}
render();
Expand Down Expand Up @@ -1090,7 +1151,7 @@ export async function runDashboardApp(
return;
}

// ── (1d) Break-category picker ([m]): pick from ALL categories ─────────
// ── (1d) Break-category picker ([c]): pick from ALL categories ─────────
if (state.breakpicker.open) {
// Ctrl-C closes the picker without exiting the app.
if (key.name === "char" && key.char === CTRL_C) {
Expand Down Expand Up @@ -1182,12 +1243,9 @@ export async function runDashboardApp(
return;
}

// [m] — open the picker for ALL categories (incl. coffee/sleep).
if (ch === "m") {
state.breakpicker = openBreakPickerState(
live.timer.isOnBreak ? live.timer.currentBreakCategory : undefined,
);
render();
// [e] — edit the running session's goal/details/target/budget live.
if (ch === "e") {
openLiveEditForm();
return;
}

Expand Down Expand Up @@ -1215,15 +1273,13 @@ export async function runDashboardApp(
return;
}

if (ch === (ctx.config.keybindings.category ?? "c")) {
if (live.timer.isOnBreak) {
const current = live.timer.currentBreakCategory;
const idx = ALL_BREAK_CATEGORIES.indexOf(current);
const next =
ALL_BREAK_CATEGORIES[(idx + 1) % ALL_BREAK_CATEGORIES.length] ?? "rest";
live.timer.setBreakCategory(next);
writeLiveJournal();
}
// [c] — open the category picker (all categories, incl. coffee/sleep).
// On break it pre-selects the current category; off break, picking one
// starts a break in that category.
if (ch === (kb.category ?? "c")) {
state.breakpicker = openBreakPickerState(
live.timer.isOnBreak ? live.timer.currentBreakCategory : undefined,
);
render();
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/tui/breakpicker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Break-category picker — a pure, self-contained transient overlay for the live
* session view. It lets the user pick any category from ALL_BREAK_CATEGORIES
* (the "more" picker that unlocks coffee/sleep beyond the 1–6 quick keys).
* (the [c] categories picker that unlocks coffee/sleep beyond the 1–6 quick keys).
*
* Mirrors the confirm/palette design: types + pure functions only, no I/O. The
* caller (app.ts) wires the action that runs on pick/cancel.
Expand Down
Loading
Loading