From c125218b761e9bf7be03be86792f5dab06119796 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 17 Jun 2026 15:47:25 -0600 Subject: [PATCH 1/4] =?UTF-8?q?feat(v3.9.0):=20foundation=20=E2=80=94=20se?= =?UTF-8?q?ssion=20form=20gains=20create/edit=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `mode: "create" | "edit"` to SessionFormState so the new-session form can double as the live-edit form. `openSessionFormState({ mode, values })` pre-fills and selects the mode; render swaps the title (New/Edit session) and the Enter verb (start/save). Back-compat: no-arg open = blank create. Pure/additive — no app wiring yet. Covered by new edit-mode tests. Co-Authored-By: Claude Opus 4.8 --- src/tui/breakpicker.ts | 2 +- src/tui/sessionform.ts | 40 +++++++++++++++++++++++-------- test/sessionform.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/tui/breakpicker.ts b/src/tui/breakpicker.ts index 6dcd6b1..13f17a4 100644 --- a/src/tui/breakpicker.ts +++ b/src/tui/breakpicker.ts @@ -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. diff --git a/src/tui/sessionform.ts b/src/tui/sessionform.ts index 3712abc..474ccea 100644 --- a/src/tui/sessionform.ts +++ b/src/tui/sessionform.ts @@ -1,10 +1,13 @@ /** - * New-session form — a pure, self-contained, transient overlay for the TUI - * dashboard. Lets the user pick a goal, name, focus target and break budget - * for a session WITHOUT dropping to the shell, then start it in the dashboard. + * Session form — a pure, self-contained, transient overlay for the TUI + * dashboard. Lets the user set a goal, details, focus target and break budget + * WITHOUT dropping to the shell. Used in two modes: + * - "create": start a brand-new session in the dashboard. + * - "edit": adjust the goal/details/target/budget of the LIVE session that + * is already running (the timer keeps counting). * * Mirrors the palette's design: types + pure functions only, no I/O. The caller - * (app.ts) does the wiring (parsing durations, creating the Timer, etc.). + * (app.ts) does the wiring (parsing durations, creating/mutating the Timer). */ import type { Key } from "../lib/tui/input.js"; @@ -41,8 +44,12 @@ const LABEL_WIDTH = 13; // "Break budget" is the longest label // State // --------------------------------------------------------------------------- +/** "create" starts a new session; "edit" mutates the running one. */ +export type SessionFormMode = "create" | "edit"; + export interface SessionFormState { open: boolean; + mode: SessionFormMode; values: SessionFormValues; active: number; // index into SESSION_FORM_FIELDS /** Cursor position (code units) within the active field's value. */ @@ -54,6 +61,7 @@ export interface SessionFormState { export function emptySessionFormState(): SessionFormState { return { open: false, + mode: "create", values: { goal: "", label: "", target: "", break: "" }, active: 0, cursor: 0, @@ -61,9 +69,20 @@ export function emptySessionFormState(): SessionFormState { }; } -/** Open a fresh form. */ -export function openSessionFormState(): SessionFormState { - return { ...emptySessionFormState(), open: true }; +/** + * Open a form. With no args it opens a blank "create" form (back-compat). Pass + * `mode: "edit"` and pre-filled `values` to edit a running session. + */ +export function openSessionFormState( + opts: { mode?: SessionFormMode; values?: Partial } = {}, +): SessionFormState { + const base = emptySessionFormState(); + return { + ...base, + open: true, + mode: opts.mode ?? "create", + values: { ...base.values, ...opts.values }, + }; } // --------------------------------------------------------------------------- @@ -196,14 +215,15 @@ export function renderSessionForm( body.push(truncate(" ⚠ " + state.error, innerW)); } - // Spacer + footer + // Spacer + footer (verb depends on mode) body.push(padTo("", innerW)); - body.push(truncate(" [Tab] next · [Enter] start · [Esc] cancel", innerW)); + const verb = state.mode === "edit" ? "save" : "start"; + body.push(truncate(` [Tab] next · [Enter] ${verb} · [Esc] cancel`, innerW)); const boxHeight = body.length + 2; // 2 borders const panelRows = panel({ - title: "New session", + title: state.mode === "edit" ? "Edit session" : "New session", width: boxWidth, height: boxHeight, body, diff --git a/test/sessionform.test.ts b/test/sessionform.test.ts index fdf2a48..c9793fa 100644 --- a/test/sessionform.test.ts +++ b/test/sessionform.test.ts @@ -154,3 +154,55 @@ describe("renderSessionForm", () => { } }); }); + +// --------------------------------------------------------------------------- +// Edit mode (v3.9.0 — editing a running session) +// --------------------------------------------------------------------------- + +describe("sessionForm — edit mode", () => { + it("defaults to create mode with no args (back-compat)", () => { + expect(openSessionFormState().mode).toBe("create"); + expect(emptySessionFormState().mode).toBe("create"); + }); + + it("opens in edit mode pre-filled with the supplied values", () => { + const s = openSessionFormState({ + mode: "edit", + values: { goal: "Deep work", label: "Korvex", target: "1h30m", break: "20m" }, + }); + expect(s.open).toBe(true); + expect(s.mode).toBe("edit"); + expect(s.values).toEqual({ goal: "Deep work", label: "Korvex", target: "1h30m", break: "20m" }); + }); + + it("fills only the provided fields, leaving the rest blank", () => { + const s = openSessionFormState({ mode: "edit", values: { goal: "Just work" } }); + expect(s.values).toEqual({ goal: "Just work", label: "", target: "", break: "" }); + }); + + it("preserves the mode through editing keystrokes", () => { + let s = openSessionFormState({ mode: "edit", values: { goal: "x" } }); + s = sessionFormApplyKey(s, ch("y")).state; + s = sessionFormApplyKey(s, { name: "tab" }).state; + expect(s.mode).toBe("edit"); + }); + + it("submit carries the current values regardless of mode", () => { + const s = openSessionFormState({ mode: "edit", values: { goal: "g", label: "", target: "", break: "" } }); + const r = sessionFormApplyKey(s, { name: "enter" }); + expect(r.action).toEqual({ type: "submit", values: { goal: "g", label: "", target: "", break: "" } }); + }); + + it("renders 'Edit session' title and '[Enter] save' footer in edit mode", () => { + const joined = renderSessionForm( + openSessionFormState({ mode: "edit", values: { goal: "g" } }), + 80, + 24, + "neon", + false, + ).rows.join("\n"); + expect(joined).toContain("Edit session"); + expect(joined).toContain("[Enter] save"); + expect(joined).not.toContain("New session"); + }); +}); From 4757c336d26425223295533b7d028b833c2f8e13 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 17 Jun 2026 15:47:34 -0600 Subject: [PATCH 2/4] feat(v3.9.0): live-edit [e], [c] category picker, centered footer, header trim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [e] during a live session opens the form in edit mode (pre-filled goal/ details/target/budget); submit applies without stopping the timer and re-seeds the crash-recovery journal. Edit-mode Ctrl-C closes the modal but keeps the session alive (only create-mode Ctrl-C exits). - [m] "more" → [c] "categories": [c] opens the break-category picker in both break states. Removes the redundant [c] cycle binding (picker supersedes it). - Footer is centered (padTo center) and the live control row is trimmed to the essentials so it fits/centers in a small terminal; full keymap stays in Help. z/Enter/Tab/r/1-6 handlers are unchanged — only their footer hints moved. - Header reads "Flowclock" (dropped the redundant "Dashboard"). Co-Authored-By: Claude Opus 4.8 --- src/tui/app.ts | 112 ++++++++++++++++++++++++++++++----------- src/tui/views/help.ts | 6 +-- test/dashboard.test.ts | 62 ++++++++++++++++++++--- 3 files changed, 141 insertions(+), 39 deletions(-) diff --git a/src/tui/app.ts b/src/tui/app.ts index 800ece5..6987201 100644 --- a/src/tui/app.ts +++ b/src/tui/app.ts @@ -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"; @@ -100,7 +100,7 @@ const VIEW_LABELS: Record = { }; /** 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. */ @@ -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; @@ -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; @@ -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) { @@ -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) { @@ -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) @@ -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. @@ -1049,11 +1102,18 @@ 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") { @@ -1061,7 +1121,8 @@ export async function runDashboardApp( return; } if (result.action?.type === "submit") { - startSessionFromForm(result.action.values); + if (mode === "edit") applyLiveEdit(result.action.values); + else startSessionFromForm(result.action.values); return; } render(); @@ -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) { @@ -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; } @@ -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; } diff --git a/src/tui/views/help.ts b/src/tui/views/help.ts index cdeb737..07d0fc4 100644 --- a/src/tui/views/help.ts +++ b/src/tui/views/help.ts @@ -41,9 +41,9 @@ export function renderHelp(rect: Rect, theme: ThemeName, color: boolean): string // Session controls body.push(padTo("Session controls (while a session runs):", innerW)); - body.push(padTo("p pause · b break · 1-6 quick category · m more categories · x cancel · z zen · Enter hide · r reset · q stop & save", innerW)); - body.push(padTo("quick: 1 rest · 2 meal · 3 exercise · 4 walk · 5 distraction · 6 other · m picker adds: coffee · sleep", innerW)); - body.push(padTo("x cancels without saving (test sessions); q stops & saves.", innerW)); + body.push(padTo("p pause · b break · 1-6 quick category · c categories · e edit · x cancel · z zen · Enter hide · r reset · q stop & save", innerW)); + body.push(padTo("quick: 1 rest · 2 meal · 3 exercise · 4 walk · 5 distraction · 6 other · c picker adds: coffee · sleep", innerW)); + body.push(padTo("e edits goal/details/target/budget mid-session; x cancels without saving (test sessions); q stops & saves.", innerW)); // Spacer body.push(""); diff --git a/test/dashboard.test.ts b/test/dashboard.test.ts index b62c354..6fb958c 100644 --- a/test/dashboard.test.ts +++ b/test/dashboard.test.ts @@ -27,7 +27,7 @@ import { renderBreaks } from "../src/tui/views/breaks.js"; import { buildFrame, compositeOverlay } from "../src/tui/app.js"; import type { LiveSession, ViewName } from "../src/tui/app.js"; import { emptyPaletteState } from "../src/tui/palette.js"; -import { emptySessionFormState } from "../src/tui/sessionform.js"; +import { emptySessionFormState, openSessionFormState } from "../src/tui/sessionform.js"; import { emptyEditFormState, openEditFormState } from "../src/tui/editform.js"; import { emptyConfirmState, openConfirmState } from "../src/tui/confirm.js"; import { emptyBreakPickerState, openBreakPickerState } from "../src/tui/breakpicker.js"; @@ -768,10 +768,11 @@ describe("buildFrame (WS5)", () => { }; } - it("live footer shows [z] zen and [Enter] hide hints", () => { + it("live footer shows compact controls (pause, break, cat, edit, cancel, stop)", () => { const out = buildFrame(fcState(), 100, 24, makeFakeCtx()).map(stripAnsi).join("\n"); - expect(out).toContain("zen"); - expect(out).toContain("hide"); + for (const hint of ["pause", "break", "cat", "edit", "cancel", "stop"]) { + expect(out).toContain(hint); + } }); it("hideControls blanks the footer controls (no 'pause')", () => { @@ -779,11 +780,56 @@ describe("buildFrame (WS5)", () => { expect(out).not.toContain("pause"); }); - // ── v3.8.0: live footer advertises the picker + cancel controls ──────────── - it("live footer shows [m] (picker) and [x] cancel", () => { + // ── v3.9.0: [c] opens the category picker; [e] edits the live session ────── + it("live footer shows [c] cat and [e] edit (no legacy [m] more)", () => { + const out = buildFrame(fcState(), 100, 24, makeFakeCtx()).map(stripAnsi).join("\n"); + expect(out).toContain("[c] cat"); + expect(out).toContain("[e] edit"); + expect(out).not.toContain("[m]"); + }); + + // ── v3.9.0: footer is centered (leading padding before the controls) ────── + it("centers the footer control row", () => { + const rows = buildFrame(fcState(), 100, 24, makeFakeCtx()).map(stripAnsi); + const footer = rows.find((r) => r.includes("[p] pause")); + expect(footer).toBeDefined(); + const leading = footer!.length - footer!.trimStart().length; + expect(leading).toBeGreaterThan(0); + }); + + // ── v3.9.0: on-break footer advertises [c] cat, resume and [e] edit ─────── + it("on-break footer shows [c] cat, resume and [e] edit", () => { + const t = new Timer(); + t.startBreak("rest", null, null); + const live: LiveSession = { + timer: t, goal: "g", label: null, focusTargetS: null, breakBudgetS: null, + }; + const out = buildFrame(fcState({ live }), 100, 24, makeFakeCtx()).map(stripAnsi).join("\n"); + expect(out).toContain("[c] cat"); + expect(out).toContain("resume"); + expect(out).toContain("[e] edit"); + }); + + // ── v3.9.0: header drops the redundant "Dashboard" suffix ───────────────── + it("header reads 'Flowclock' without the 'Dashboard' suffix", () => { const out = buildFrame(fcState(), 100, 24, makeFakeCtx()).map(stripAnsi).join("\n"); - expect(out).toContain("[m]"); - expect(out).toContain("cancel"); + expect(out).toContain("Flowclock"); + expect(out).not.toContain("Flowclock Dashboard"); + }); + + // ── v3.9.0: editing the running session composites an "Edit session" form ── + it("live-edit form overlay shows 'Edit session', the pre-filled value and save footer", () => { + const state = { + ...fcState(), + form: openSessionFormState({ + mode: "edit" as const, + values: { goal: "Korvex", label: "", target: "1h", break: "20m" }, + }), + }; + const out = buildFrame(state, 100, 24, makeFakeCtx()).map(stripAnsi).join("\n"); + expect(out).toContain("Edit session"); + expect(out).toContain("Korvex"); + expect(out).toContain("[Enter] save"); }); // ── v3.8.0: break-category picker overlay composites over the frame ──────── From dfce98cda6afa9cfccbde305e2debf30f15078f0 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 17 Jun 2026 15:54:28 -0600 Subject: [PATCH 3/4] docs(v3.9.0): version bump, CHANGELOG, README (live edit, [c] picker, footer) Bump to 3.9.0; CHANGELOG 3.9.0 section; README updated; v3.9.0 roadmap row. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 31 ++++++++++++++++++++++++------- package.json | 2 +- src/version.ts | 2 +- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4895c9c..2e3a3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c3337ad..0d273a5 100644 --- a/README.md +++ b/README.md @@ -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 ║ ║ ║ @@ -199,10 +199,13 @@ 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 | @@ -210,14 +213,17 @@ Live session controls inside the dashboard: | `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 @@ -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) @@ -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) │ @@ -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 ` 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 | diff --git a/package.json b/package.json index b43393f..3dbcbd6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/version.ts b/src/version.ts index 805223e..63ca7de 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ /** Kept in sync with package.json "version" (asserted by a test). */ -export const VERSION = "3.8.0"; +export const VERSION = "3.9.0"; From 78acbc198851de65b0f912d8175972d36b4db30d Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 17 Jun 2026 16:02:19 -0600 Subject: [PATCH 4/4] fix(deps): pin hono >=4.12.25 to clear high-sev advisory (CI audit gate) A high-severity advisory landed against hono <=4.12.24 since v3.8.0; pulled in transitively via @modelcontextprotocol/sdk. flowclock's MCP server is stdio-only, so the vulnerable HTTP/serve-static paths are not exercised, but the npm audit --audit-level=high release gate blocks regardless. Pin to 4.12.25 via overrides. Patch bump; build + 725 tests green. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 10 +++++----- package.json | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4c72d1..7661a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flowclock-cli", - "version": "0.1.0", + "version": "3.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flowclock-cli", - "version": "0.1.0", + "version": "3.9.0", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", @@ -3277,9 +3277,9 @@ } }, "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/package.json b/package.json index 3dbcbd6..a4d3776 100644 --- a/package.json +++ b/package.json @@ -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",