From 6f554ffde45196352099c04942262cf29e60350c Mon Sep 17 00:00:00 2001 From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com> Date: Tue, 19 May 2026 15:54:16 +1000 Subject: [PATCH 01/11] feat: show date on prediction suggestion preview display --- .../ui/frontend/src/components/PredictionSuggestion.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx index 23d582e..c3d65d1 100644 --- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx +++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx @@ -60,7 +60,7 @@ function suggestion_range_format( || start.getDate() !== end.getDate(); if (!crosses_local_day) { - return `${time_format(block_start)} → ${time_format(block_end)}`; + return `${suggestion_range_part_format(start)} → ${time_format(block_end)}`; } return `${suggestion_range_part_format(start)} → ${suggestion_range_part_format(end)}`; @@ -349,8 +349,7 @@ export const PredictionSuggestion: Component<{ "margin-bottom": "1px", }} > - {time_format(item.block_start)} →{" "} - {time_format(item.block_end)} + {suggestion_range_format(item.block_start, item.block_end)} {item.suggested} From 36ce3e7274fafa3607ff4e907af898d9efefbbdd Mon Sep 17 00:00:00 2001 From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com> Date: Tue, 19 May 2026 15:56:50 +1000 Subject: [PATCH 02/11] refactor: tighten block_start/end type --- .../src/components/PredictionSuggestion.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx index c3d65d1..386cc16 100644 --- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx +++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx @@ -40,19 +40,9 @@ function suggestion_range_part_format(d: Date): string { }); } -function suggestion_range_format( - block_start: string | null | undefined, - block_end: string | null | undefined, -): string { - if (!block_start || !block_end) { - return `${time_format(block_start)} → ${time_format(block_end)}`; - } - +function suggestion_range_format(block_start: string, block_end: string): string { const start = new Date(block_start); const end = new Date(block_end); - if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { - return `${time_format(block_start)} → ${time_format(block_end)}`; - } const crosses_local_day = start.getFullYear() !== end.getFullYear() @@ -257,6 +247,10 @@ export const PredictionSuggestion: Component<{ const sg = s(); return sg ? { start: sg.block_start, end: sg.block_end } : null; }; + const active_suggestion_range = () => { + const sg = s(); + return sg ? suggestion_range_format(sg.block_start, sg.block_end) : ""; + }; return ( @@ -403,7 +397,7 @@ export const PredictionSuggestion: Component<{ "margin-top": "4px", }} > - {suggestion_range_format(s()?.block_start, s()?.block_end)} + {active_suggestion_range()}
Date: Tue, 19 May 2026 16:56:20 +1000 Subject: [PATCH 03/11] refactor: nullish coalescing to default states and null chaining with early return --- src/taskclf/ui/frontend/biome.json | 2 +- .../components/PredictionSuggestion.test.tsx | 23 +- .../src/components/PredictionSuggestion.tsx | 768 ++++++++++-------- 3 files changed, 434 insertions(+), 359 deletions(-) diff --git a/src/taskclf/ui/frontend/biome.json b/src/taskclf/ui/frontend/biome.json index fae34b7..29dd07b 100644 --- a/src/taskclf/ui/frontend/biome.json +++ b/src/taskclf/ui/frontend/biome.json @@ -27,7 +27,7 @@ "noExplicitAny": "warn" }, "complexity": { - "useOptionalChain": "error" + "useOptionalChain": "off" }, "nursery": { "noJsxPropsBind": "off" diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx index fd9fb40..8453c05 100644 --- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx +++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx @@ -92,6 +92,15 @@ function suggestion_make(overrides: Partial = {}): LabelSuggest }; } +function suggestion_range_part_text(d: Date): string { + return d.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + function suggestion_range_text(block_start: string, block_end: string): string { const start = new Date(block_start); const end = new Date(block_end); @@ -101,20 +110,10 @@ function suggestion_range_text(block_start: string, block_end: string): string { || start.getDate() !== end.getDate(); if (!crosses_local_day) { - return `${time_format(block_start)} → ${time_format(block_end)}`; + return `${suggestion_range_part_text(start)} → ${time_format(block_end)}`; } - return `${start.toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - })} → ${end.toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - })}`; + return `${suggestion_range_part_text(start)} → ${suggestion_range_part_text(end)}`; } function overlap_error_make(): Error { diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx index 386cc16..36b8254 100644 --- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx +++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx @@ -31,6 +31,14 @@ const btn_base = { "font-weight": "600", }; +function label_color(name: string, fallback = "var(--text)"): string { + const color = LABEL_COLORS[name]; + if (color) { + return color; + } + return fallback; +} + function suggestion_range_part_format(d: Date): string { return d.toLocaleString(undefined, { month: "short", @@ -72,16 +80,36 @@ export const PredictionSuggestion: Component<{ const [overwrite_pending, set_overwrite_pending] = createSignal(null); const [change_label_open, set_change_label_open] = createSignal(false); - const [selected_label, set_selected_label] = createSignal(null); + const [selected_label, set_selected_label] = createSignal(""); const [labels] = createResource(core_labels_list); + + const label_choices = () => { + const loaded = labels(); + if (!loaded) { + return []; + } + return loaded; + }; + const pending_suggestions = () => { + if (props.suggestions) { + return props.suggestions(); + } const sg = s(); - return props.suggestions?.() ?? (sg ? [sg] : []); + if (!sg) { + return []; + } + return [sg]; }; + const active_key = () => { const sg = s(); - return sg ? label_suggestion_key(sg) : null; + if (!sg) { + return null; + } + return label_suggestion_key(sg); }; + const active_index = () => { const key = active_key(); if (!key) { @@ -91,30 +119,67 @@ export const PredictionSuggestion: Component<{ (item) => label_suggestion_key(item) === key, ); }; + const pending_count = () => pending_suggestions().length; + const suggestion_position = () => { const idx = active_index(); - return idx >= 0 ? idx + 1 : 0; + if (idx < 0) { + return 0; + } + return idx + 1; }; + const correction_label = () => selected_label(); + + function notify_saved() { + if (props.on_saved) { + props.on_saved(); + } + } + + function notify_dismiss( + reason: SuggestionClearReason, + suggestion: LabelSuggestion | null, + ) { + if (props.on_dismiss) { + props.on_dismiss(reason, suggestion); + } + } + + function select_suggestion(item: LabelSuggestion) { + if (props.on_select) { + props.on_select(item); + } + } + + function reset_for_suggestion(sg: LabelSuggestion) { + set_overwrite_pending(null); + set_change_label_open(false); + set_selected_label(sg.suggested); + set_error(null); + } + createEffect( on( () => { const sg = s(); - return sg ? `${sg.block_start}:${sg.block_end}:${sg.suggested}` : ""; + if (!sg) { + return ""; + } + return `${sg.block_start}:${sg.block_end}:${sg.suggested}`; }, () => { const sg = s(); - set_overwrite_pending(null); - set_change_label_open(false); - set_selected_label(sg?.suggested ?? null); - set_error(null); + if (!sg) { + set_selected_label(""); + return; + } + reset_for_suggestion(sg); }, ), ); - const correction_label = () => selected_label() ?? s()?.suggested ?? ""; - async function suggestion_save(label: string) { const sg = s(); if (!sg || busy() || !label) { @@ -123,22 +188,23 @@ export const PredictionSuggestion: Component<{ set_busy(true); set_error(null); try { - await notification_accept({ - ...(sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + const payload = { block_start: sg.block_start, block_end: sg.block_end, label, - }); + ...(sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + }; + await notification_accept(payload); set_overwrite_pending(null); set_change_label_open(false); - props.on_saved?.(); - props.on_dismiss?.("label_saved", sg); + notify_saved(); + notify_dismiss("label_saved", sg); } catch (err: unknown) { const pending = overwrite_pending_from_api_error(err, { label, start: sg.block_start, end: sg.block_end, - confidence: sg.confidence ?? 1, + confidence: sg.confidence, extend_forward: false, }); if (pending) { @@ -170,16 +236,17 @@ export const PredictionSuggestion: Component<{ set_error(null); try { const sg = s(); - await notification_accept({ - ...(sg?.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + const payload = { block_start: pending.start, block_end: pending.end, label: pending.label, - overwrite: true, - }); + overwrite: true as const, + ...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + }; + await notification_accept(payload); set_overwrite_pending(null); - props.on_saved?.(); - props.on_dismiss?.("label_saved", sg); + notify_saved(); + notify_dismiss("label_saved", sg); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "overwrite failed"; frontend_log_error("Failed to overwrite with suggested label", err); @@ -198,16 +265,17 @@ export const PredictionSuggestion: Component<{ set_error(null); try { const sg = s(); - await notification_accept({ - ...(sg?.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + const payload = { block_start: pending.start, block_end: pending.end, label: pending.label, - allow_overlap: true, - }); + allow_overlap: true as const, + ...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}), + }; + await notification_accept(payload); set_overwrite_pending(null); - props.on_saved?.(); - props.on_dismiss?.("label_saved", sg); + notify_saved(); + notify_dismiss("label_saved", sg); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "keep all failed"; frontend_log_error("Failed to keep all with suggested label", err); @@ -225,12 +293,14 @@ export const PredictionSuggestion: Component<{ set_busy(true); set_error(null); try { - await notification_skip( - sg?.suggestion_id ? { suggestion_id: sg.suggestion_id } : undefined, - ); + if (sg && sg.suggestion_id) { + await notification_skip({ suggestion_id: sg.suggestion_id }); + } else { + await notification_skip(undefined); + } set_overwrite_pending(null); set_change_label_open(false); - props.on_dismiss?.("skipped", sg); + notify_dismiss("skipped", sg); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "Failed to dismiss suggestion"; frontend_log_error("Failed to dismiss suggestion", err); @@ -240,386 +310,392 @@ export const PredictionSuggestion: Component<{ } } + function open_change_label(sg: LabelSuggestion) { + set_error(null); + set_selected_label(sg.suggested); + set_change_label_open(true); + } + + function cancel_change_label(sg: LabelSuggestion) { + set_change_label_open(false); + set_selected_label(sg.suggested); + } + const busy_opacity = () => (busy() ? "0.6" : "1"); const busy_cursor = () => (busy() ? "not-allowed" : "pointer"); const suggestion_time_range = () => { const sg = s(); - return sg ? { start: sg.block_start, end: sg.block_end } : null; - }; - const active_suggestion_range = () => { - const sg = s(); - return sg ? suggestion_range_format(sg.block_start, sg.block_end) : ""; + if (!sg) { + return null; + } + return { start: sg.block_start, end: sg.block_end }; }; return ( -
- 1}> -
-
- Model suggestions ({pending_count()} pending) - - {suggestion_position()} of {pending_count()} - -
-
    - - {(item, index) => { - const is_active = () => label_suggestion_key(item) === active_key(); - return ( -
  • - -
  • - ); - }} -
    -
-
-
+ {(active) => (
-
-
- - Model suggests a change:{" "} - - - {s()?.old_label} - - - - {s()?.suggested} - - - {" "} - ({Math.round((s()?.confidence ?? 0) * 100)}%) - -
+ 1}>
- {active_suggestion_range()} -
-
-
- - - -
-
- -
-
- What should this time block be labeled instead? -
- - Loading label choices... -
- } - > -
+ Model suggestions ({pending_count()} pending) + + {suggestion_position()} of {pending_count()} + +
+
    - - {(label_name) => { - const is_selected = () => label_name === correction_label(); - const is_suggested = () => label_name === s()?.suggested; + + {(item, index) => { + const is_active = () => label_suggestion_key(item) === active_key(); return ( - + {item.suggested} + + ); }} - - +
+
+
+
+
+
+ + Model suggests a change:{" "} + + + {active().old_label} + + + + {active().suggested} + + + {" "} + ({Math.round(active().confidence * 100)}%) + +
+
+ {suggestion_range_format(active().block_start, active().block_end)} +
+
+
- - - - {(pending) => ( - set_overwrite_pending(null)} - /> - )} - - - set_error(null)} /> - - + +
+
+ What should this time block be labeled instead? +
+ + Loading label choices... +
+ } + > +
+ + {(label_name) => { + const is_selected = () => label_name === correction_label(); + const is_suggested = () => label_name === active().suggested; + return ( + + ); + }} + +
+
+
+ + +
+ + + + + {(pending) => ( + set_overwrite_pending(null)} + /> + )} + + + {(message) => ( + set_error(null)} /> + )} + + + )} ); }; From 096662b2fbd30fb7e81743314cf83b4786502c20 Mon Sep 17 00:00:00 2001 From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com> Date: Tue, 19 May 2026 16:59:59 +1000 Subject: [PATCH 04/11] refactor: frontend null to undefined --- src/taskclf/ui/frontend/src/App.test.tsx | 172 +++++++-------- src/taskclf/ui/frontend/src/App.tsx | 16 +- .../src/components/ActivitySummary.test.tsx | 8 +- .../src/components/ActivitySummary.tsx | 30 ++- .../frontend/src/components/ErrorBanner.tsx | 8 +- .../ui/frontend/src/components/LabelForm.tsx | 13 +- .../src/components/LabelHistory.test.tsx | 6 +- .../frontend/src/components/LabelHistory.tsx | 54 ++--- .../src/components/LabelHistoryGapRow.tsx | 10 +- .../src/components/LabelHistoryRow.tsx | 22 +- .../src/components/LabelHistoryTimeline.tsx | 6 +- .../ui/frontend/src/components/LabelLast.tsx | 2 +- .../ui/frontend/src/components/LabelQueue.tsx | 2 +- .../src/components/LabelRecorder.test.tsx | 28 +-- .../frontend/src/components/LabelRecorder.tsx | 70 +++--- .../LabelRecorderActivitySummary.test.tsx | 16 +- .../ui/frontend/src/components/LabelTable.tsx | 2 +- .../src/components/LabelTimePicker.tsx | 6 +- .../src/components/PredictionBadge.test.tsx | 24 +-- .../src/components/PredictionBadge.tsx | 10 +- .../components/PredictionSuggestion.test.tsx | 24 +-- .../src/components/PredictionSuggestion.tsx | 49 ++--- .../frontend/src/components/StatusPanel.tsx | 4 +- .../frontend/src/components/TrainingPanel.tsx | 63 +++--- .../status/StatusActivityMonitor.tsx | 2 +- .../status/StatusActivityWatch.test.tsx | 8 +- .../src/components/status/StatusModel.tsx | 19 +- .../components/status/StatusPrediction.tsx | 2 +- .../components/status/StatusSuggestion.tsx | 2 +- src/taskclf/ui/frontend/src/lib/api.ts | 50 ++--- src/taskclf/ui/frontend/src/lib/date.test.ts | 2 +- src/taskclf/ui/frontend/src/lib/date.ts | 10 +- src/taskclf/ui/frontend/src/lib/format.ts | 10 +- src/taskclf/ui/frontend/src/lib/host.ts | 8 +- .../ui/frontend/src/lib/labelTimeline.ts | 8 +- .../label_overwrite_pending_upd_get.test.ts | 20 +- .../lib/label_overwrite_pending_upd_get.ts | 8 +- .../ui/frontend/src/lib/notifications.test.ts | 12 +- .../ui/frontend/src/lib/notifications.ts | 4 +- src/taskclf/ui/frontend/src/lib/nullish.ts | 24 +++ .../overwrite_pending_from_api_error.test.ts | 6 +- .../lib/overwrite_pending_from_api_error.ts | 8 +- .../src/lib/transitionPromptNotifications.ts | 2 +- src/taskclf/ui/frontend/src/lib/ws.test.ts | 28 +-- src/taskclf/ui/frontend/src/lib/ws.ts | 200 +++++++++--------- .../ui/frontend/src/test/ws_store_stub.ts | 46 ++-- 46 files changed, 591 insertions(+), 533 deletions(-) create mode 100644 src/taskclf/ui/frontend/src/lib/nullish.ts diff --git a/src/taskclf/ui/frontend/src/App.test.tsx b/src/taskclf/ui/frontend/src/App.test.tsx index 1e25d02..0d71fc2 100644 --- a/src/taskclf/ui/frontend/src/App.test.tsx +++ b/src/taskclf/ui/frontend/src/App.test.tsx @@ -169,8 +169,8 @@ describe("App drag regions", () => { type: "status", state: "idle", current_app: "unknown", - current_app_since: null, - candidate_app: null, + current_app_since: undefined, + candidate_app: undefined, candidate_duration_s: 0, transition_threshold_s: 0, poll_seconds: 0, @@ -183,7 +183,7 @@ describe("App drag regions", () => { state: "checking", summary_available: false, endpoint: "http://localhost:5600", - source_id: null, + source_id: undefined, last_sample_count: 0, last_sample_breakdown: {}, setup_title: "Activity source unavailable", @@ -197,30 +197,30 @@ describe("App drag regions", () => { help_url: "https://activitywatch.net/", }, aw_connected: false, - aw_bucket_id: null, + aw_bucket_id: undefined, aw_host: "http://localhost:5600", last_event_count: 0, last_app_counts: {}, }), - latest_prediction: () => null, + latest_prediction: () => undefined, latest_tray_state: () => ({ type: "tray_state", model_loaded: false, - model_dir: null, - model_schema_hash: null, - suggested_label: null, - suggested_confidence: null, + model_dir: undefined, + model_schema_hash: undefined, + suggested_label: undefined, + suggested_confidence: undefined, transition_count: 0, - last_transition: null, + last_transition: undefined, labels_saved_count: 0, data_dir: "~/.taskclf", ui_port: 0, dev_mode: false, paused: false, }), - active_suggestion: () => null, - latest_prompt: () => null, - live_status: () => null, + active_suggestion: () => undefined, + latest_prompt: () => undefined, + live_status: () => undefined, label_grid_requested: () => 0, connection_status: () => "connected", ws_stats: () => ({ @@ -229,19 +229,19 @@ describe("App drag regions", () => { prediction_count: 0, tray_state_count: 0, suggestion_count: 0, - last_message_at: null, + last_message_at: undefined, reconnect_count: 0, - connected_since: null, + connected_since: undefined, }), train_state: () => ({ - job_id: null, + job_id: undefined, status: "idle", - step: null, - progress_pct: null, - message: null, - error: null, - metrics: null, - model_dir: null, + step: undefined, + progress_pct: undefined, + message: undefined, + error: undefined, + metrics: undefined, + model_dir: undefined, }), suggestion_dismiss: vi.fn(), }), @@ -291,8 +291,8 @@ describe("App drag regions", () => { type: "status", state: "idle", current_app: "unknown", - current_app_since: null, - candidate_app: null, + current_app_since: undefined, + candidate_app: undefined, candidate_duration_s: 0, transition_threshold_s: 0, poll_seconds: 0, @@ -305,7 +305,7 @@ describe("App drag regions", () => { state: "checking", summary_available: false, endpoint: "http://localhost:5600", - source_id: null, + source_id: undefined, last_sample_count: 0, last_sample_breakdown: {}, setup_title: "Activity source unavailable", @@ -319,30 +319,30 @@ describe("App drag regions", () => { help_url: "https://activitywatch.net/", }, aw_connected: false, - aw_bucket_id: null, + aw_bucket_id: undefined, aw_host: "http://localhost:5600", last_event_count: 0, last_app_counts: {}, }), - latest_prediction: () => null, + latest_prediction: () => undefined, latest_tray_state: () => ({ type: "tray_state", model_loaded: false, - model_dir: null, - model_schema_hash: null, - suggested_label: null, - suggested_confidence: null, + model_dir: undefined, + model_schema_hash: undefined, + suggested_label: undefined, + suggested_confidence: undefined, transition_count: 0, - last_transition: null, + last_transition: undefined, labels_saved_count: 0, data_dir: "~/.taskclf", ui_port: 0, dev_mode: false, paused: false, }), - active_suggestion: () => null, - latest_prompt: () => null, - live_status: () => null, + active_suggestion: () => undefined, + latest_prompt: () => undefined, + live_status: () => undefined, label_grid_requested: () => 0, connection_status: () => "connected", ws_stats: () => ({ @@ -351,19 +351,19 @@ describe("App drag regions", () => { prediction_count: 0, tray_state_count: 0, suggestion_count: 0, - last_message_at: null, + last_message_at: undefined, reconnect_count: 0, - connected_since: null, + connected_since: undefined, }), train_state: () => ({ - job_id: null, + job_id: undefined, status: "idle", - step: null, - progress_pct: null, - message: null, - error: null, - metrics: null, - model_dir: null, + step: undefined, + progress_pct: undefined, + message: undefined, + error: undefined, + metrics: undefined, + model_dir: undefined, }), suggestion_dismiss: vi.fn(), }), @@ -426,8 +426,8 @@ describe("App drag regions", () => { type: "status", state: "idle", current_app: "unknown", - current_app_since: null, - candidate_app: null, + current_app_since: undefined, + candidate_app: undefined, candidate_duration_s: 0, transition_threshold_s: 0, poll_seconds: 0, @@ -440,7 +440,7 @@ describe("App drag regions", () => { state: "checking", summary_available: false, endpoint: "http://localhost:5600", - source_id: null, + source_id: undefined, last_sample_count: 0, last_sample_breakdown: {}, setup_title: "Activity source unavailable", @@ -454,30 +454,30 @@ describe("App drag regions", () => { help_url: "https://activitywatch.net/", }, aw_connected: false, - aw_bucket_id: null, + aw_bucket_id: undefined, aw_host: "http://localhost:5600", last_event_count: 0, last_app_counts: {}, }), - latest_prediction: () => null, + latest_prediction: () => undefined, latest_tray_state: () => ({ type: "tray_state", model_loaded: false, - model_dir: null, - model_schema_hash: null, - suggested_label: null, - suggested_confidence: null, + model_dir: undefined, + model_schema_hash: undefined, + suggested_label: undefined, + suggested_confidence: undefined, transition_count: 0, - last_transition: null, + last_transition: undefined, labels_saved_count: 0, data_dir: "~/.taskclf", ui_port: 0, dev_mode: false, paused: false, }), - active_suggestion: () => null, + active_suggestion: () => undefined, latest_prompt: () => prompt_store, - live_status: () => null, + live_status: () => undefined, label_grid_requested: () => 0, connection_status: () => "connected", ws_stats: () => ({ @@ -486,19 +486,19 @@ describe("App drag regions", () => { prediction_count: 0, tray_state_count: 0, suggestion_count: 0, - last_message_at: null, + last_message_at: undefined, reconnect_count: 0, - connected_since: null, + connected_since: undefined, }), train_state: () => ({ - job_id: null, + job_id: undefined, status: "idle", - step: null, - progress_pct: null, - message: null, - error: null, - metrics: null, - model_dir: null, + step: undefined, + progress_pct: undefined, + message: undefined, + error: undefined, + metrics: undefined, + model_dir: undefined, }), suggestion_dismiss: vi.fn(), }), @@ -556,8 +556,8 @@ describe("App drag regions", () => { type: "status", state: "idle", current_app: "unknown", - current_app_since: null, - candidate_app: null, + current_app_since: undefined, + candidate_app: undefined, candidate_duration_s: 0, transition_threshold_s: 0, poll_seconds: 0, @@ -570,7 +570,7 @@ describe("App drag regions", () => { state: "checking", summary_available: false, endpoint: "http://localhost:5600", - source_id: null, + source_id: undefined, last_sample_count: 0, last_sample_breakdown: {}, setup_title: "Activity source unavailable", @@ -584,30 +584,30 @@ describe("App drag regions", () => { help_url: "https://activitywatch.net/", }, aw_connected: false, - aw_bucket_id: null, + aw_bucket_id: undefined, aw_host: "http://localhost:5600", last_event_count: 0, last_app_counts: {}, }), - latest_prediction: () => null, + latest_prediction: () => undefined, latest_tray_state: () => ({ type: "tray_state", model_loaded: false, - model_dir: null, - model_schema_hash: null, - suggested_label: null, - suggested_confidence: null, + model_dir: undefined, + model_schema_hash: undefined, + suggested_label: undefined, + suggested_confidence: undefined, transition_count: 0, - last_transition: null, + last_transition: undefined, labels_saved_count: 0, data_dir: "~/.taskclf", ui_port: 0, dev_mode: false, paused: false, }), - active_suggestion: () => null, + active_suggestion: () => undefined, latest_prompt: () => prompt, - live_status: () => null, + live_status: () => undefined, label_grid_requested: () => 0, connection_status: () => "connected", ws_stats: () => ({ @@ -616,19 +616,19 @@ describe("App drag regions", () => { prediction_count: 0, tray_state_count: 0, suggestion_count: 0, - last_message_at: null, + last_message_at: undefined, reconnect_count: 0, - connected_since: null, + connected_since: undefined, }), train_state: () => ({ - job_id: null, + job_id: undefined, status: "idle", - step: null, - progress_pct: null, - message: null, - error: null, - metrics: null, - model_dir: null, + step: undefined, + progress_pct: undefined, + message: undefined, + error: undefined, + metrics: undefined, + model_dir: undefined, }), suggestion_dismiss: vi.fn(), }), diff --git a/src/taskclf/ui/frontend/src/App.tsx b/src/taskclf/ui/frontend/src/App.tsx index 8d2114a..0f3942a 100644 --- a/src/taskclf/ui/frontend/src/App.tsx +++ b/src/taskclf/ui/frontend/src/App.tsx @@ -73,8 +73,8 @@ const App: Component = () => { const [panel_hovered, set_panel_hovered] = createSignal(false); const label_visible = () => label_pinned() || badge_hovered() || label_hovered(); const panel_visible = () => panel_pinned() || dot_hovered() || panel_hovered(); - let label_hide_timer: ReturnType | null = null; - let panel_hide_timer: ReturnType | null = null; + let label_hide_timer: ReturnType | undefined; + let panel_hide_timer: ReturnType | undefined; const open_label_grid = () => { if (browser_compact) { @@ -85,16 +85,16 @@ const App: Component = () => { }; const label_hide_cancel = () => { - if (label_hide_timer !== null) { + if (label_hide_timer !== undefined) { clearTimeout(label_hide_timer); - label_hide_timer = null; + label_hide_timer = undefined; } }; const panel_hide_cancel = () => { - if (panel_hide_timer !== null) { + if (panel_hide_timer !== undefined) { clearTimeout(panel_hide_timer); - panel_hide_timer = null; + panel_hide_timer = undefined; } }; @@ -106,7 +106,7 @@ const App: Component = () => { label_hide_timer = setTimeout(() => { set_badge_hovered(false); set_label_hovered(false); - label_hide_timer = null; + label_hide_timer = undefined; }, CHILD_HIDE_DELAY_MS); }; @@ -118,7 +118,7 @@ const App: Component = () => { panel_hide_timer = setTimeout(() => { set_dot_hovered(false); set_panel_hovered(false); - panel_hide_timer = null; + panel_hide_timer = undefined; }, CHILD_HIDE_DELAY_MS); }; diff --git a/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx b/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx index 472f004..4ff169a 100644 --- a/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx +++ b/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx @@ -44,9 +44,9 @@ function activity_summary_make( activity_provider: activity_provider_make(activity_provider), recent_apps: [], top_apps: [], - mean_keys_per_min: null, - mean_clicks_per_min: null, - mean_scroll_per_min: null, + mean_keys_per_min: undefined, + mean_clicks_per_min: undefined, + mean_scroll_per_min: undefined, total_buckets: 0, session_count: 0, range_state: "no_data", @@ -77,7 +77,7 @@ describe("ActivitySummary", () => { activity_provider: activity_provider_make({ state: "setup_required", summary_available: false, - source_id: null, + source_id: undefined, }), range_state: "provider_unavailable", message: diff --git a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx index 1bb95f8..34ab7b5 100644 --- a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx +++ b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx @@ -46,22 +46,24 @@ const PredictionBadge: Component<{ p: Accessor }> = (props) => ( export const ActivitySummary: Component<{ minutes?: Accessor; - time_range?: Accessor; - prediction?: Accessor; + time_range?: Accessor; + prediction?: Accessor; show_empty?: boolean; }> = (props) => { const range = () => props.time_range?.() - ?? (props.minutes ? time_range_minutes(props.minutes()) : null); + ?? (props.minutes ? time_range_minutes(props.minutes()) : undefined); - const [summary, set_summary] = createSignal(null); + const [summary, set_summary] = createSignal( + undefined, + ); const [is_loading, set_is_loading] = createSignal(false); const [request_failed, set_request_failed] = createSignal(false); createEffect(() => { const r = range(); if (!r) { - set_summary(null); + set_summary(undefined); set_is_loading(false); set_request_failed(false); return; @@ -83,7 +85,7 @@ export const ActivitySummary: Component<{ if (cancelled) { return; } - set_summary(null); + set_summary(undefined); set_request_failed(true); set_is_loading(false); }); @@ -94,7 +96,7 @@ export const ActivitySummary: Component<{ }); const pred = () => props.prediction?.(); - const provider = () => summary()?.activity_provider ?? null; + const provider = () => summary()?.activity_provider ?? undefined; const recent_apps = () => (summary()?.recent_apps ?? []).slice(0, 3); const has_recent_apps = () => recent_apps().length > 0; const feature_apps = () => (summary()?.top_apps ?? []).slice(0, 5); @@ -102,7 +104,9 @@ export const ActivitySummary: Component<{ const has_apps = () => has_recent_apps() || has_feature_apps(); const has_stats = () => { const s = summary(); - return s && (s.mean_keys_per_min != null || s.mean_clicks_per_min != null); + return ( + s && (s.mean_keys_per_min !== undefined || s.mean_clicks_per_min !== undefined) + ); }; const has_coverage = () => { const s = summary(); @@ -271,13 +275,17 @@ export const ActivitySummary: Component<{ "flex-wrap": "wrap", }} > - + {(v) => keys {v()}/m} - + {(v) => clicks {v()}/m} - + {(v) => scroll {v()}/m} diff --git a/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx b/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx index 0e82081..4255ee2 100644 --- a/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx +++ b/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx @@ -36,10 +36,10 @@ export const ErrorBanner: Component<{ const [copy_state, set_copy_state] = createSignal<"idle" | "copied" | "failed">( "idle", ); - let reset_timer: ReturnType | null = null; + let reset_timer: ReturnType | undefined; onCleanup(() => { - if (reset_timer !== null) { + if (reset_timer !== undefined) { clearTimeout(reset_timer); } }); @@ -52,12 +52,12 @@ export const ErrorBanner: Component<{ frontend_log_error("Failed to copy error message", err); set_copy_state("failed"); } finally { - if (reset_timer !== null) { + if (reset_timer !== undefined) { clearTimeout(reset_timer); } reset_timer = setTimeout(() => { set_copy_state("idle"); - reset_timer = null; + reset_timer = undefined; }, 2000); } } diff --git a/src/taskclf/ui/frontend/src/components/LabelForm.tsx b/src/taskclf/ui/frontend/src/components/LabelForm.tsx index 233ec85..9f42064 100644 --- a/src/taskclf/ui/frontend/src/components/LabelForm.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelForm.tsx @@ -28,14 +28,17 @@ export const LabelForm: Component = () => { const [end_ts, set_end_ts] = createSignal(""); const [label, set_label] = createSignal(""); const [confidence, set_confidence] = createSignal(0.8); - const [status, set_status] = createSignal<{ - type: "success" | "error"; - msg: string; - } | null>(null); + const [status, set_status] = createSignal< + | { + type: "success" | "error"; + msg: string; + } + | undefined + >(undefined); async function label_submit(e: Event) { e.preventDefault(); - set_status(null); + set_status(undefined); try { const result = await label_create({ start_ts: start_ts(), diff --git a/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx b/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx index 767a8fe..082306a 100644 --- a/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx @@ -38,7 +38,7 @@ describe("LabelHistory", () => { end_ts: iso_at_local_time(date_str, 10), label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -49,7 +49,7 @@ describe("LabelHistory", () => { end_ts: iso_at_local_time(date_str, 10), label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -58,7 +58,7 @@ describe("LabelHistory", () => { end_ts: iso_at_local_time(date_str, 11), label: "Write", provenance: "suggestion", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, diff --git a/src/taskclf/ui/frontend/src/components/LabelHistory.tsx b/src/taskclf/ui/frontend/src/components/LabelHistory.tsx index fe69582..aa57615 100644 --- a/src/taskclf/ui/frontend/src/components/LabelHistory.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelHistory.tsx @@ -75,7 +75,7 @@ export const LabelHistory: Component<{ date_str: effective_selected_date(), label_change_count: props.label_change_count?.() ?? 0, } - : null, + : undefined, async (source) => { if (!source) { return []; @@ -85,10 +85,10 @@ export const LabelHistory: Component<{ ); const [coreLabels] = createResource(core_labels_list); - const [expanded_key, set_expanded_key] = createSignal(null); + const [expanded_key, set_expanded_key] = createSignal(undefined); const [busy, set_busy] = createSignal(false); - const [flash, set_flash] = createSignal(null); - const [error, set_error] = createSignal(null); + const [flash, set_flash] = createSignal(undefined); + const [error, set_error] = createSignal(undefined); const day_data = createMemo(() => { const l = labels(); @@ -103,9 +103,9 @@ export const LabelHistory: Component<{ function row_toggle(item: TimelineItem) { const key = item_key(item); - set_expanded_key(expanded_key() === key ? null : key); - set_flash(null); - set_error(null); + set_expanded_key(expanded_key() === key ? undefined : key); + set_flash(undefined); + set_error(undefined); } async function label_update_submit( @@ -115,8 +115,8 @@ export const LabelHistory: Component<{ new_end: string, ) { set_busy(true); - set_flash(null); - set_error(null); + set_flash(undefined); + set_error(undefined); try { await label_update({ start_ts: item.start_ts, @@ -127,8 +127,8 @@ export const LabelHistory: Component<{ }); set_flash(new_label); setTimeout(() => { - set_flash(null); - set_expanded_key(null); + set_flash(undefined); + set_expanded_key(undefined); refetch(); }, 800); } catch (err: unknown) { @@ -140,14 +140,14 @@ export const LabelHistory: Component<{ async function label_delete_submit(item: LabelItem) { set_busy(true); - set_flash(null); - set_error(null); + set_flash(undefined); + set_error(undefined); try { await label_delete({ start_ts: item.start_ts, end_ts: item.end_ts, }); - set_expanded_key(null); + set_expanded_key(undefined); refetch(); } catch (err: unknown) { set_error(err instanceof Error ? err.message : String(err)); @@ -158,8 +158,8 @@ export const LabelHistory: Component<{ async function gap_create_submit(start_ts: string, end_ts: string, label: string) { set_busy(true); - set_flash(null); - set_error(null); + set_flash(undefined); + set_error(undefined); try { await label_create({ start_ts, @@ -168,8 +168,8 @@ export const LabelHistory: Component<{ }); set_flash(label); setTimeout(() => { - set_flash(null); - set_expanded_key(null); + set_flash(undefined); + set_expanded_key(undefined); refetch(); }, 800); } catch (err: unknown) { @@ -320,9 +320,9 @@ export const LabelHistory: Component<{ return; } const key = item_key(item); - set_expanded_key(expanded_key() === key ? null : key); - set_flash(null); - set_error(null); + set_expanded_key(expanded_key() === key ? undefined : key); + set_flash(undefined); + set_error(undefined); }} /> @@ -338,9 +338,9 @@ export const LabelHistory: Component<{ on_create={gap_create_submit} core_labels={coreLabels() ?? []} busy={busy()} - flash={expanded_key() === item_key(item) ? flash() : null} - error={expanded_key() === item_key(item) ? error() : null} - on_error_close={() => set_error(null)} + flash={expanded_key() === item_key(item) ? flash() : undefined} + error={expanded_key() === item_key(item) ? error() : undefined} + on_error_close={() => set_error(undefined)} /> } > @@ -355,9 +355,9 @@ export const LabelHistory: Component<{ on_delete={() => label_delete_submit(item as LabelItem)} core_labels={coreLabels() ?? []} busy={busy()} - flash={expanded_key() === item_key(item) ? flash() : null} - error={expanded_key() === item_key(item) ? error() : null} - on_error_close={() => set_error(null)} + flash={expanded_key() === item_key(item) ? flash() : undefined} + error={expanded_key() === item_key(item) ? error() : undefined} + on_error_close={() => set_error(undefined)} /> )} diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx index 8f2a25b..7b7e671 100644 --- a/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx @@ -21,8 +21,8 @@ export const LabelHistoryGapRow: Component<{ on_create: (start: string, end: string, label: string) => void; core_labels: string[]; busy: boolean; - flash: string | null; - error: string | null; + flash: string | undefined; + error: string | undefined; on_error_close: () => void; }> = (props) => { const gap_start_d = () => date_parse(props.gap.start_ts); @@ -56,16 +56,16 @@ export const LabelHistoryGapRow: Component<{ set_end_iso(time_input_date(props.date_str, val).toISOString()); } - const selected_range = (): TimeRange | null => { + const selected_range = (): TimeRange | undefined => { const s = date_parse(start_iso()).getTime(); const e = date_parse(end_iso()).getTime(); if (e <= s) { - return null; + return undefined; } return { start: start_iso(), end: end_iso() }; }; - const range_valid = () => selected_range() !== null; + const range_valid = () => selected_range() !== undefined; const time_input_style = { background: "#111", diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx index 2d8ba0f..8b5577c 100644 --- a/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx @@ -22,8 +22,8 @@ export const LabelHistoryRow: Component<{ on_delete: () => void; core_labels: string[]; busy: boolean; - flash: string | null; - error: string | null; + flash: string | undefined; + error: string | undefined; on_error_close: () => void; }> = (props) => { const is_open_ended = () => props.label_item.open_ended === true; @@ -34,7 +34,9 @@ export const LabelHistoryRow: Component<{ ? "until next label" : duration_fmt(end_d().getTime() - start_d().getTime()); const [confirm_delete, set_confirm_delete] = createSignal(false); - const [pending_label, set_pending_label] = createSignal(null); + const [pending_label, set_pending_label] = createSignal( + undefined, + ); const [start_time, set_start_time] = createSignal(time_input_value(start_d())); const [end_time, set_end_time] = createSignal(time_input_value(end_d())); @@ -44,23 +46,23 @@ export const LabelHistoryRow: Component<{ set_end_time(time_input_value(end_d())); }); - const edited_range = (): TimeRange | null => { + const edited_range = (): TimeRange | undefined => { const start = time_input_date(props.date_str, start_time()); const end = time_input_date(props.date_str, end_time()); if (end.getTime() <= start.getTime()) { - return null; + return undefined; } return { start: start.toISOString(), end: end.toISOString() }; }; - const range_valid = () => edited_range() !== null; + const range_valid = () => edited_range() !== undefined; const time_changed = () => start_time() !== time_input_value(start_d()) || end_time() !== time_input_value(end_d()); const label_changed = () => - pending_label() !== null && pending_label() !== props.label_item.label; + pending_label() !== undefined && pending_label() !== props.label_item.label; const effective_label = () => pending_label() ?? props.label_item.label; const has_changes = () => label_changed() || time_changed(); @@ -245,7 +247,7 @@ export const LabelHistoryRow: Component<{ onClick={(e) => { e.stopPropagation(); if (label_name === props.label_item.label) { - set_pending_label(null); + set_pending_label(undefined); } else { set_pending_label(label_name); } @@ -335,7 +337,7 @@ export const LabelHistoryRow: Component<{ disabled={props.busy} onClick={(e) => { e.stopPropagation(); - set_pending_label(null); + set_pending_label(undefined); set_start_time(time_input_value(start_d())); set_end_time(time_input_value(end_d())); }} @@ -359,7 +361,7 @@ export const LabelHistoryRow: Component<{ const r = edited_range(); if (r) { const label = effective_label(); - set_pending_label(null); + set_pending_label(undefined); props.on_update(label, r.start, r.end); } }} diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx index 8cd46dd..76218d1 100644 --- a/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx @@ -7,7 +7,9 @@ export const LabelHistoryTimeline: Component<{ segments: TimelineSegment[]; on_segment_click?: (seg: TimelineSegment, index: number) => void; }> = (props) => { - const [tooltip, set_tooltip] = createSignal<{ text: string; x: number } | null>(null); + const [tooltip, set_tooltip] = createSignal<{ text: string; x: number } | undefined>( + undefined, + ); return (
@@ -55,7 +57,7 @@ export const LabelHistoryTimeline: Component<{ } }} onMouseLeave={(e) => { - set_tooltip(null); + set_tooltip(undefined); if (!seg.label) { e.currentTarget.style.background = "rgba(255,255,255,0.04)"; } diff --git a/src/taskclf/ui/frontend/src/components/LabelLast.tsx b/src/taskclf/ui/frontend/src/components/LabelLast.tsx index b81a0b2..7fabb93 100644 --- a/src/taskclf/ui/frontend/src/components/LabelLast.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelLast.tsx @@ -11,7 +11,7 @@ type LabelLastProps = { end_ts: string; extend_forward?: boolean; } - | null + | undefined | undefined >; is_current?: Accessor; diff --git a/src/taskclf/ui/frontend/src/components/LabelQueue.tsx b/src/taskclf/ui/frontend/src/components/LabelQueue.tsx index 5572d50..0b72560 100644 --- a/src/taskclf/ui/frontend/src/components/LabelQueue.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelQueue.tsx @@ -71,7 +71,7 @@ export const LabelQueue: Component = () => { > {item.reason} {item.predicted_label && ` · ${item.predicted_label}`} - {item.confidence !== null + {item.confidence !== undefined && ` · ${Math.round(item.confidence * 100)}%`}
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx index 414757d..0b420a5 100644 --- a/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx @@ -22,14 +22,14 @@ vi.mock("./ActivitySummary", () => ({ })); vi.mock("./PredictionSuggestion", () => ({ - PredictionSuggestion: () => null, + PredictionSuggestion: () => undefined, })); beforeEach(() => { vi.clearAllMocks(); vi.useRealTimers(); vi.mocked(core_labels_list).mockResolvedValue(["Build", "Write"]); - vi.mocked(current_label_get).mockResolvedValue(null); + vi.mocked(current_label_get).mockResolvedValue(undefined); }); describe("LabelRecorder", () => { @@ -43,7 +43,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:00:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }, @@ -54,7 +54,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T10:00:00.000Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -65,17 +65,17 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:00:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }) - .mockResolvedValueOnce(null); + .mockResolvedValueOnce(undefined); vi.mocked(label_update).mockResolvedValue({ start_ts: "2026-04-05T09:00:00Z", end_ts: "2026-04-05T10:00:00.000Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }); @@ -123,7 +123,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:05:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }, @@ -133,7 +133,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:05:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }); @@ -153,7 +153,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T10:00:00Z", label: "Write", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -163,7 +163,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:00:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }); @@ -184,7 +184,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:30:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -216,7 +216,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T09:30:00Z", label: "Build", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: false, }, @@ -226,7 +226,7 @@ describe("LabelRecorder", () => { end_ts: "2026-04-05T11:00:00.000Z", label: "Write", provenance: "manual", - user_id: null, + user_id: undefined, confidence: 1, extend_forward: true, }); diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx index 854739b..4355212 100644 --- a/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx +++ b/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx @@ -44,13 +44,13 @@ function extend_forward_pref_read(): boolean { type LabelRecorderProps = { max_height?: number; on_collapse: () => void; - prediction?: Accessor; - suggestion?: Accessor; + prediction?: Accessor; + suggestion?: Accessor; suggestions?: Accessor; label_change_count?: Accessor; on_suggestion_dismiss?: ( reason?: SuggestionClearReason, - suggestion?: LabelSuggestion | null, + suggestion?: LabelSuggestion, ) => void; on_suggestion_select?: (suggestion: LabelSuggestion) => void; }; @@ -63,22 +63,23 @@ export const LabelRecorder: Component = (props) => { const [last_ended_label] = createResource(label_refresh_key, async () => { try { const rows = await labels_list(1); - return rows.length ? rows[0] : null; + return rows.length ? rows[0] : undefined; } catch { - return null; + return undefined; } }); const [current_label_result] = createResource(label_refresh_key, async () => { try { return await current_label_get(); } catch { - return null; + return undefined; } }); - const [flash, set_flash] = createSignal(null); - const [error, set_error] = createSignal(null); - const [overwrite_pending, set_overwrite_pending] = - createSignal(null); + const [flash, set_flash] = createSignal(undefined); + const [error, set_error] = createSignal(undefined); + const [overwrite_pending, set_overwrite_pending] = createSignal< + OverwritePending | undefined + >(undefined); const [selected_minutes, set_selected_minutes] = createSignal(0); const [extend_fwd, set_extend_fwd] = createSignal(extend_forward_pref_read()); const [fill_from_last, set_fill_from_last] = createSignal(false); @@ -86,16 +87,16 @@ export const LabelRecorder: Component = (props) => { const [stop_current_pending, set_stop_current_pending] = createSignal(false); const [stop_current_busy, set_stop_current_busy] = createSignal(false); - const current_label = () => current_label_result() ?? null; + const current_label = () => current_label_result() ?? undefined; - const footer_label = () => current_label() ?? last_ended_label() ?? null; + const footer_label = () => current_label() ?? last_ended_label() ?? undefined; createEffect( on( () => [last_ended_label(), current_label()] as const, () => { if (overwrite_pending()) { - set_overwrite_pending(null); + set_overwrite_pending(undefined); } if (stop_current_pending()) { set_stop_current_pending(false); @@ -119,7 +120,7 @@ export const LabelRecorder: Component = (props) => { { selected_minutes: selected_minutes(), fill_from_last: fill_from_last(), - last_label_end_ts: last_ended_label()?.end_ts ?? null, + last_label_end_ts: last_ended_label()?.end_ts ?? undefined, extend_fwd: extend_fwd(), }, new Date(), @@ -155,7 +156,7 @@ export const LabelRecorder: Component = (props) => { start = new Date(now.getTime() - mins * 60_000); } const effective_extend = force_extend_fwd || extend_fwd(); - set_error(null); + set_error(undefined); try { await label_create({ start_ts: start.toISOString(), @@ -166,7 +167,7 @@ export const LabelRecorder: Component = (props) => { }); set_flash(label); set_label_version((v) => v + 1); - setTimeout(() => set_flash(null), 1500); + setTimeout(() => set_flash(undefined), 1500); } catch (err: unknown) { const pending = overwrite_pending_from_api_error(err, { label, @@ -189,8 +190,8 @@ export const LabelRecorder: Component = (props) => { if (!pending) { return; } - set_overwrite_pending(null); - set_error(null); + set_overwrite_pending(undefined); + set_error(undefined); try { await label_create({ start_ts: pending.start, @@ -202,7 +203,7 @@ export const LabelRecorder: Component = (props) => { }); set_flash(pending.label); set_label_version((v) => v + 1); - setTimeout(() => set_flash(null), 1500); + setTimeout(() => set_flash(undefined), 1500); } catch (err: unknown) { set_error(err instanceof Error ? err.message : "overwrite failed"); } @@ -213,8 +214,8 @@ export const LabelRecorder: Component = (props) => { if (!pending) { return; } - set_overwrite_pending(null); - set_error(null); + set_overwrite_pending(undefined); + set_error(undefined); try { await label_create({ start_ts: pending.start, @@ -226,7 +227,7 @@ export const LabelRecorder: Component = (props) => { }); set_flash(pending.label); set_label_version((v) => v + 1); - setTimeout(() => set_flash(null), 1500); + setTimeout(() => set_flash(undefined), 1500); } catch (err: unknown) { set_error(err instanceof Error ? err.message : "keep all failed"); } @@ -242,8 +243,8 @@ export const LabelRecorder: Component = (props) => { const stop_ts = new Date(now_ms <= start_ms ? start_ms + 1 : now_ms).toISOString(); set_stop_current_busy(true); - set_error(null); - set_flash(null); + set_error(undefined); + set_flash(undefined); try { await label_update({ start_ts: current.start_ts, @@ -266,14 +267,14 @@ export const LabelRecorder: Component = (props) => { style={{ padding: "8px", "border-top": "1px solid var(--border)", - ...(props.max_height != null + ...(props.max_height !== undefined ? { "max-height": `${props.max_height}px`, "overflow-y": "auto" } : {}), }} > null)} + suggestion={props.suggestion ?? (() => undefined)} suggestions={props.suggestions} on_saved={() => set_label_version((v) => v + 1)} on_dismiss={props.on_suggestion_dismiss} @@ -286,7 +287,7 @@ export const LabelRecorder: Component = (props) => { set_selected_minutes={set_selected_minutes} fill_from_last={fill_from_last} set_fill_from_last={set_fill_from_last} - has_current_label={() => current_label() != null} + has_current_label={() => current_label() !== undefined} last_label={last_ended_label} /> @@ -304,7 +305,7 @@ export const LabelRecorder: Component = (props) => { pending={pending()} on_confirm={overwrite_confirm} on_keep_all={keep_all_confirm} - on_cancel={() => set_overwrite_pending(null)} + on_cancel={() => set_overwrite_pending(undefined)} /> )} @@ -312,7 +313,7 @@ export const LabelRecorder: Component = (props) => {