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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- **History visibility controls.** The History tab now defaults to a privacy-safe Telemetry view, adds an Exposed view for request/result text that was actually stored, and surfaces the redacted/visible capture setting in context. Rows captured while redacted remain text-free.

## 2.2.0

**Prompt builder controls.** Flowkey's `prompt:` mode can now target Claude Code or generic chat tools without exposing raw built-in system-prompt editing.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ The dashboard is a web page served by the local daemon — open it from the tray
- **Prompt builder:** Config → Prompt builder controls the built-in `prompt:` output style without allowing raw edits to the locked system prompt.
- **Models:** pull models with live progress — pick a suggestion or type any name (on Ollama, anything from the [library](https://ollama.com/library) works); set active, remove. Suggestions are hardware-aware: detected RAM/VRAM caps the model size (e.g. 32 GB RAM → ~4B on the NPU; 8 GB VRAM → ~9B on the GPU), oversized models are hidden, and free-typing one asks before pulling.
- **Benchmark:** works on both providers — `flm bench` on FastFlowLM (~10–20 min, NPU), timed generations with native metrics on Ollama (~1–3 min, server keeps running).
- **Notes:** browse or search your vault; History shows recent runs (text is stored only if history storage is enabled).
- **History:** Telemetry view shows what ran and how fast; Exposed view shows stored request/result text only for rows captured while history storage was visible. The History tab includes the same redacted/visible storage toggle as Config.
- **Notes:** browse or search your vault.
- **Meetings:** connect the local [Quill](https://quillapp.com) app to search meetings, read AI digests (pre-computed after-hours), review action items (accept / reject), and generate a weekly review. Off by default — enable in Config → Meetings.
- **Notifications:** per-event toggles, dedupe window, Do-Not-Disturb, and quiet hours; every toast (shown or muted) is logged to the Telemetry feed.

Expand Down
4 changes: 4 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Caveman-encoded (compression, not amputation). Paths / ids / action names / numb
- api: `POST /action/<name>` ! header `X-FFP-API: 1` → 200 `{ok,result,error,elapsed_ms}`
- api: `GET /` → dashboard; `GET /healthz` → `{ok,version,api,actions}`
- action: `config_snapshot` → full cfg; `apply_config_patch {patch}` → merge (whitelist `filter_config_patch`)
- action: `recent_history {limit?}` → newest history rows; `input_text`/`output_text` iff stored @ write-time
- action: `prompt_builder_preview {settings?,sample?}` → deterministic local preview (`⊥` LLM call)
- action: `notify_gate {title,message}` → `{show,reason,category}` (logs); `notifications_log {limit}` → rows
- action: `quill_status` → `{reachable,enabled,server,server_version}`
Expand Down Expand Up @@ -70,6 +71,8 @@ Caveman-encoded (compression, not amputation). Paths / ids / action names / numb
- V22: `sync.ps1` ∃ uncommitted tracked changes → skip pull (⊥ clobber un-pushed WIP)
- V23: meeting `NoContentError` & age ≥ 2d → skip-marker (`meeting_skips.jsonl`) ∴ ⊥ re-queue ever; age < 2d → retry (Quill transcript may still sync); non-content errors ⊥ skip-mark. batch errors → `daemon.log` only (⊥ UI panel, per user)
- V24: prompt_builder default cfg ⇒ `CLAUDE_PROMPT_SYSTEM_PROMPT` identity; non-default prompt validation target-aware (XML only when effective structure=xml); built-in `modes.prompt.system_prompt` still locked
- V25: History view toggle = display-only; ⊥ mutate `history_store_text`; ⊥ reveal text absent from jsonl row
- V26: History tab load default view = Telemetry (text hidden) even when storage visible

## §T tasks

Expand All @@ -96,6 +99,7 @@ T18|.|[DOCS] provider roadmap marks selector/status UX incomplete → update to
T19|x|[DOCS] first-run wizard text "chat popup" → "Open chat" + hotkey now `^!c` (2.1.1)|B5
T20|x|[DOCS] daemon log location — audited 2.1.1: no stale ref in README/docs; nothing to change|—
T21|x|prompt_builder cfg + claude_code identity + generic_chat adapter + dashboard controls/preview|V17,V24
T22|x|History Telemetry/Exposed views + inline redacted/visible storage control/help|V5,V6,V25,V26
```

## §B bugs
Expand Down
220 changes: 220 additions & 0 deletions docs/history-visibility-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Dashboard History Visibility Plan (hidden vs exposed views)

Date: 2026-07-09
Target release: **Unreleased** after `v2.2.0`
Status: IMPLEMENTED + VALIDATED locally — pending commit

## Goal

Give the History tab clear visibility with two explicit view modes:

- **Telemetry view** (default, privacy-safe): what/when/how-fast, no captured text.
- **Exposed view**: reveals captured request/result text for entries that
actually stored it.

Plus surface the text-storage setting *inside* the History tab so users
understand why text is or isn't present, and can change it in context.

## Implementation status (2026-07-09)

Implemented:

- `recent_history` passes through `input_text` and `output_text` only when those
fields exist on the jsonl row.
- History tab defaults to **Telemetry** on every load.
- History tab has an **Exposed** view for stored request/result text and shows
"not stored" for rows captured while redacted.
- History tab surfaces the redacted/visible write-time capture policy with an
inline toggle using existing daemon actions.
- Config tab storage checkbox has a reusable keyboard-accessible help marker.
- `SPEC.md`, README, CHANGELOG, and tests updated.

Verified:

- `python -m py_compile scripts\ffp_telemetry.py scripts\ffp_daemon.py`
- `node --check scripts\ui\web\app.js`
- `python -m ruff check scripts tests` → all checks passed
- `python -m pytest tests -q` → 347 passed
- `AutoHotkey64.exe /ErrorStdOut tests\test_parse_mode.ahk`
- `AutoHotkey64.exe /ErrorStdOut tests\test_classify_clipboard.ahk`

## Current state (verified in code, 2026-07-09)

- **Setting: `history_store_text`** (boolean, default `false`), a WRITE-time flag.
- Config UI: `#cfg-store-text` checkbox "Store selected text (off = redacted)"
(`scripts/ui/web/index.html:338`), saved via `apply_config_patch`
(`app.js:861`); whitelisted in `filter_config_patch` (`ffp_config.py:504`).
- Tray menu: "🙈 Redacted" / visible (`tray.ahk`), daemon actions
`set_history_visible` / `set_history_redacted` / `toggle_history_text` /
`history_text_status`.
- Overview shows read-only state (`app.js:249`).
- **Write behavior** (`grammar_fix.py` ~1048-1057): every run always stores
telemetry (`mode`, `input_chars`, `output_chars`, `elapsed_seconds`,
`tok_per_sec`, `completion_tokens`, `timestamp`). Only when
`HISTORY_STORE_TEXT` is true does it also store `input_text` and
`output_text`.
- **History tab** (`app.js` `loadHistory` → `recent_history` action):
renders a Telemetry table by default and an Exposed table on request. Stored
`input_text`/`output_text` is visible only in Exposed view; missing fields show
"not stored".
- Privacy invariant **V6**: history text redacted by default
(`history_store_text` false).

**Net:** the storage toggle is now surfaced where its effect is visible, and
the view toggle remains independent from the write-time storage policy.

## Key design constraint (must be honest about this)

Redaction is decided at WRITE time. Therefore:

- The **exposed view can only reveal text that was already stored.** Rows
captured while redacted have no text on disk — toggling the view (or the
setting) later does **not** retroactively reveal them.
- The view toggle is a **display preference**; the storage setting is a
**capture policy**. They are independent and must never be conflated:
switching to exposed view must not change storage; changing storage must not
alter existing rows.

So each entry falls into one of three display states:
1. **Telemetry-only** (captured while redacted) → exposed view shows "— (not
stored)".
2. **Text stored** (captured while visible) → exposed view shows the text.
3. Future: **result stored** (if output-text storage is added) → exposed view
shows request + result.

## Proposed UX

History tab gains:

1. **A view switch** (segmented control / two tabs): **Telemetry** (default) and
**Exposed**. Default is always Telemetry on load — privacy-first display.
2. **A storage-state banner** at the top of the tab:
- Redacted: "Text storage: **Redacted** — new runs store telemetry only.
[Change in Config]" (or an inline toggle).
- Visible: "Text storage: **Visible** — new runs store your captured text.
[Change]".
This is where the answer to "where is the setting?" lives — brought to the
tab where its effect is felt.
3. **Exposed view rendering:** each row expands (or adds columns) to show the
captured request text; rows without stored text show a muted "— (not
stored — captured while redacted)". If output-text storage is added, show the
result too.
4. **Exposed-view affordance:** because it renders potentially sensitive text on
screen, gate the first switch with a one-line in-page notice (not a blocking
confirm) — "Showing stored request text." No native confirm (V5).

Telemetry view = today's 7-column table, unchanged.

## Inline setting help (hover "read me")

Problem: the Config label **"Store selected text (off = redacted)"** is easy to
miss and doesn't explain what it does — a user can lose track of what it means
or where it is.

Add a small, reusable **help affordance** next to settings: an unobtrusive info
marker (e.g. `ⓘ`) after the label that reveals a short explanation on
**hover and keyboard focus**. Ship it on the storage checkbox first; make the
pattern reusable so other cryptic Config settings can adopt it later.

Design (CSP- and V5-safe):

- A `<span class="help" tabindex="0" role="img" aria-label="What is this?">ⓘ</span>`
followed by a `<span class="help-text" role="tooltip">…</span>`, both built via
`createElement` + `textContent` (no innerHTML; content is static copy).
- Reveal purely with CSS on `:hover` and `:focus-within` (keyboard-accessible),
positioned as a small popover; no JS needed to show/hide. Link with
`aria-describedby` for screen readers.
- Keep a plain `title` attribute as a no-CSS fallback, but the styled popover is
the primary affordance (native `title` is slow and unstyled).
- No `alert`/`confirm`/`prompt`; no external assets (CSP `default-src 'self'`).

Copy for the storage setting (exact text to render):

> **Store selected text.** Controls whether the exact text you send to a hotkey
> is saved in History. **Off (default, "redacted"):** only telemetry is kept —
> mode, character counts, timing, tokens — your text is never written to disk.
> **On ("visible"):** the captured request text and generated result text are
> also saved so you can re-read them in History's Exposed view. This is a capture
> policy for *new* runs; it never reveals or hides text already recorded.
> Everything stays on your machine.

This help marker is the Config-tab half; the History-tab storage banner (above)
is the same explanation surfaced where the effect is felt. Both should read
consistently.

## Backend changes (implemented)

- `recent_history` passes through `input_text` and `output_text` when those
fields are present on a row. No redaction is re-applied at read time because
redaction already happened at write time.
- A per-entry `has_text` / display flag was not added; the frontend checks field
presence.
- Generated output text is stored under the same `HISTORY_STORE_TEXT` guard as
input text.
- All hotkey modes write to the same history file; the `mode` column keeps them
distinguishable.

## Frontend changes

- `index.html`: History tab gains the view switch, the storage-state banner, and
an exposed-view container. All DOM via createElement/textContent (V5); no
innerHTML.
- `index.html` + `styles.css`: add a **reusable help marker** (`ⓘ` + tooltip
popover, `.help` / `.help-text`, CSS `:hover`/`:focus-within` reveal) and apply
it to the Config **"Store selected text"** checkbox first. Component is generic
so other settings can adopt it later.
- `app.js`:
- `loadHistory` fetches once; render function takes a `view` arg
(telemetry|exposed).
- Telemetry render = current table.
- Exposed render = table/rows with request (and result) text, "— (not stored)"
for telemetry-only rows.
- Banner reads `history_store_text` from `config_snapshot` (or
`history_text_status`) and offers an inline change (calls
`set_history_visible` / `set_history_redacted`, then refreshes the banner and
Overview).
- Default view = telemetry on every tab load.

## Privacy invariants to add (SPEC §V)

- V(new): History **view** toggle is display-only — never changes
`history_store_text` and never reveals text that was not stored at write time.
- V(new): default History **view** is telemetry (text hidden) on load, even when
storage is visible.
- Keep V6 (redacted by default) unchanged.

## Tests

- Backend: `recent_history` returns `input_text` for rows written under visible
mode and omits it for rows written under redacted mode (no read-time reveal).
- If output-text storage is added: stored under visible mode, absent under
redacted.
- Frontend (`node --check` + a small DOM-logic unit if feasible): exposed render
shows text when present, "not stored" marker when absent; view toggle does not
call any storage-mutating action; default view is telemetry.
- Help marker: content is set via `textContent` (no innerHTML), reveals on hover
AND focus (keyboard-accessible), and is linked via `aria-describedby`; the copy
matches the History banner wording.
- Config: `history_store_text` still whitelisted, still defaults false.

## Resolved implementation choices

1. **Store output text too:** yes, under the existing `history_store_text` guard.
No separate output-text flag was added.
2. **Row-level vs global reveal:** global Exposed view with Telemetry as the
default on every load.
3. **Retention/limit:** unchanged. This batch does not add clear-history or
max-row controls.
4. **Version:** tracked as Unreleased after `v2.2.0`.

## Rollout order

1. Backend: `recent_history` text passthrough; `output_text` already stored
under the visible flag; tests.
2. Frontend: view switch + storage banner + exposed render; default telemetry;
reusable help marker (`.help`/`.help-text` in `styles.css`) on the Config
"Store selected text" checkbox.
3. Gates: `node --check` app.js, pytest, ruff.
4. SPEC: add the two view invariants; note the History tab now surfaces the
storage setting.
5. Docs: README History bullet — mention the two views and where storage is set.
2 changes: 1 addition & 1 deletion scripts/ffp_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ def _act_mode_ids(_args: dict) -> str:


def _act_recent_history(args: dict) -> list[dict]:
"""Last N history entries (summary fields only — never stored text)."""
"""Last N history entries (telemetry plus stored text when present)."""
try:
limit = int(args.get("limit") or 50)
except (TypeError, ValueError):
Expand Down
13 changes: 7 additions & 6 deletions scripts/ffp_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ def append_history(history_path: Path, entry: dict) -> None:
log.warning("append_history failed (%s): %s", history_path, exc)


# Summary fields exposed to dashboard clients. input_text / output_text are
# deliberately excluded so stored selection text can never leak into a client
# that only needs telemetry (e.g. the web dashboard's History tab).
_HISTORY_SUMMARY_KEYS = (
# Fields exposed to dashboard clients. input_text / output_text are included
# only when they were written under history_store_text=True; rows captured while
# redacted have no text to reveal at read time.
_HISTORY_DISPLAY_KEYS = (
"timestamp", "ts", "status", "mode", "source", "model", "strategy",
"input_chars", "output_chars", "elapsed_seconds", "api_time",
"prompt_tokens", "completion_tokens", "tok_per_sec",
"input_text", "output_text",
)


def recent_history(history_path: Path, limit: int = 50) -> list[dict]:
"""Last `limit` history entries, newest first, summary fields only."""
"""Last `limit` history entries, newest first, with stored text if present."""
entries: list[dict] = []
if not history_path.exists():
return entries
Expand All @@ -47,7 +48,7 @@ def recent_history(history_path: Path, limit: int = 50) -> list[dict]:
row = json.loads(raw)
except Exception:
continue
entries.append({key: row[key] for key in _HISTORY_SUMMARY_KEYS if key in row})
entries.append({key: row[key] for key in _HISTORY_DISPLAY_KEYS if key in row})
if len(entries) >= limit:
break
return entries
Expand Down
Loading