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...
-
- }
- >
-
+
-
- {(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 (
-
+
);
}}
-
-
+
+
+
+
+
+
+
+ Model suggests a change:{" "}
+
+
+ {active().old_label}
+
+ →
+
+ {active().suggested}
+
+
+ {" "}
+ ({Math.round(active().confidence * 100)}%)
+
+
+
+ {suggestion_range_format(active().block_start, active().block_end)}
+
+
{
- set_change_label_open(false);
- set_selected_label(s()?.suggested ?? null);
+ onClick={suggestion_accept}
+ style={{
+ ...btn_base,
+ border: "1px solid var(--accent, #6366f1)",
+ background: "var(--accent, #6366f1)",
+ color: "#fff",
+ opacity: busy_opacity(),
+ cursor: busy_cursor(),
}}
+ >
+ Use suggestion
+
+ open_change_label(active())}
style={{
...btn_base,
border: "1px solid var(--border)",
background: "var(--surface)",
- color: "var(--text-muted)",
+ color: "var(--text)",
opacity: busy_opacity(),
cursor: busy_cursor(),
}}
>
- Cancel
+ Change label
suggestion_save(correction_label())}
+ disabled={busy()}
+ onClick={suggestion_dismiss}
style={{
...btn_base,
- border: "1px solid var(--accent, #6366f1)",
- background: "var(--accent, #6366f1)",
- color: "#fff",
+ border: "1px solid var(--border)",
+ background: "var(--surface)",
+ color: "var(--text-muted)",
opacity: busy_opacity(),
- cursor: busy() || !correction_label() ? "not-allowed" : "pointer",
+ cursor: busy_cursor(),
}}
>
- Save as {correction_label()}
+ Skip
-
-
-
- {(pending) => (
- set_overwrite_pending(null)}
- />
- )}
-
-
- set_error(null)} />
-
-
+
+
+
+ What should this time block be labeled instead?
+
+
+ Loading label choices...
+
+ }
+ >
+
+
+
+ cancel_change_label(active())}
+ style={{
+ ...btn_base,
+ border: "1px solid var(--border)",
+ background: "var(--surface)",
+ color: "var(--text-muted)",
+ opacity: busy_opacity(),
+ cursor: busy_cursor(),
+ }}
+ >
+ Cancel
+
+ suggestion_save(correction_label())}
+ style={{
+ ...btn_base,
+ border: "1px solid var(--accent, #6366f1)",
+ background: "var(--accent, #6366f1)",
+ color: "#fff",
+ opacity: busy_opacity(),
+ cursor: busy() || !correction_label() ? "not-allowed" : "pointer",
+ }}
+ >
+ Save as {correction_label()}
+
+
+
+
+
+
+ {(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) => {
set_flash(null)}
+ onClick={() => set_flash(undefined)}
style={{
cursor: "pointer",
background: "none",
@@ -328,7 +329,7 @@ export const LabelRecorder: Component = (props) => {
- set_error(null)} />
+ set_error(undefined)} />
= (props) => {
- current_label() != null} />
+ current_label() !== undefined}
+ />
= (props) => {
type="button"
disabled={stop_current_busy()}
onClick={() => {
- set_flash(null);
- set_error(null);
+ set_flash(undefined);
+ set_error(undefined);
set_stop_current_pending(true);
}}
style={{
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
index 6738cc1..9515575 100644
--- a/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
@@ -22,15 +22,15 @@ vi.mock("../lib/api", () => ({
describe("LabelRecorder activity summary", () => {
it("keeps manual labeling interactive when the provider is unavailable", async () => {
vi.mocked(core_labels_list).mockResolvedValue(["Build", "Write"]);
- vi.mocked(current_label_get).mockResolvedValue(null);
+ vi.mocked(current_label_get).mockResolvedValue(undefined);
vi.mocked(labels_list).mockResolvedValue([]);
vi.mocked(label_update).mockResolvedValue({
start_ts: "2026-04-09T10:00:00Z",
end_ts: "2026-04-09T10:00:00Z",
label: "Build",
provenance: "manual",
- user_id: null,
- confidence: null,
+ user_id: undefined,
+ confidence: undefined,
extend_forward: false,
});
vi.mocked(label_create).mockResolvedValue({
@@ -38,7 +38,7 @@ describe("LabelRecorder activity summary", () => {
end_ts: "2026-04-09T10:00:00Z",
label: "Build",
provenance: "manual",
- user_id: null,
+ user_id: undefined,
confidence: 1,
extend_forward: false,
});
@@ -49,7 +49,7 @@ describe("LabelRecorder activity summary", () => {
state: "setup_required",
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",
@@ -64,9 +64,9 @@ describe("LabelRecorder activity summary", () => {
},
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: "provider_unavailable",
diff --git a/src/taskclf/ui/frontend/src/components/LabelTable.tsx b/src/taskclf/ui/frontend/src/components/LabelTable.tsx
index 17041b8..ab7d159 100644
--- a/src/taskclf/ui/frontend/src/components/LabelTable.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelTable.tsx
@@ -113,7 +113,7 @@ export const LabelTable: Component = () => {
color: "var(--text-muted)",
}}
>
- {row.confidence !== null
+ {row.confidence !== undefined
? `${Math.round(row.confidence * 100)}%`
: "—"}
diff --git a/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx b/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
index d8fe69e..2046e88 100644
--- a/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
@@ -25,7 +25,9 @@ type LabelTimePickerProps = {
set_fill_from_last: (v: boolean) => void;
has_current_label: Accessor
;
last_label: Accessor<
- { start_ts: string; end_ts: string; extend_forward?: boolean } | null | undefined
+ | { start_ts: string; end_ts: string; extend_forward?: boolean }
+ | undefined
+ | undefined
>;
};
@@ -48,7 +50,7 @@ export const LabelTimePicker: Component = (props) => {
const t = now_ms();
const ll = props.last_label();
if (props.has_current_label() || !ll?.end_ts || label_entry_is_open_ended(ll)) {
- return null;
+ return undefined;
}
return gap_shortcut_label_from_end(iso_date_parse(ll.end_ts).getTime(), t);
});
diff --git a/src/taskclf/ui/frontend/src/components/PredictionBadge.test.tsx b/src/taskclf/ui/frontend/src/components/PredictionBadge.test.tsx
index e3eb70a..0e05bb6 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionBadge.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionBadge.test.tsx
@@ -13,8 +13,8 @@ describe("PredictionBadge", () => {
type: "status" as const,
state: "idle" as const,
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,
@@ -27,7 +27,7 @@ describe("PredictionBadge", () => {
state: "checking" as const,
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",
@@ -41,7 +41,7 @@ describe("PredictionBadge", () => {
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: {},
@@ -49,12 +49,12 @@ describe("PredictionBadge", () => {
latest_tray_state: () => ({
type: "tray_state" as const,
model_loaded: true,
- 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,
@@ -63,16 +63,16 @@ describe("PredictionBadge", () => {
}),
badge_display_override: () => ({
enabled: false,
- label: null,
+ label: undefined,
}),
- active_suggestion: () => null,
+ active_suggestion: () => undefined,
};
it("falls back to live status when there is no latest prediction", () => {
render(() => (
null}
+ latest_prediction={() => undefined}
live_status={() => ({
type: "live_status",
label: "Write",
diff --git a/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx b/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
index c8116d2..6c520a7 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
@@ -14,11 +14,11 @@ import { ConnectionDot } from "./ConnectionDot";
export const PredictionBadge: Component<{
status: Accessor;
latest_status: Accessor;
- latest_prediction: Accessor;
- live_status: Accessor;
+ latest_prediction: Accessor;
+ live_status: Accessor;
badge_display_override?: Accessor;
latest_tray_state: Accessor;
- active_suggestion: Accessor;
+ active_suggestion: Accessor;
label_pinned?: Accessor;
panel_pinned?: Accessor;
on_toggle_panel?: () => void;
@@ -30,10 +30,10 @@ export const PredictionBadge: Component<{
}> = (props) => {
const prediction_label = () => {
const pred = props.latest_prediction();
- return pred ? pred.mapped_label || pred.label : null;
+ return pred ? pred.mapped_label || pred.label : undefined;
};
- const live_label = () => props.live_status()?.label ?? null;
+ const live_label = () => props.live_status()?.label ?? undefined;
const display_label = () => {
const override = props.badge_display_override?.();
diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
index 8453c05..b1b2b08 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
@@ -46,9 +46,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",
@@ -199,7 +199,7 @@ describe("PredictionSuggestion", () => {
activity_provider: activity_provider_make({
state: "setup_required",
summary_available: false,
- source_id: null,
+ source_id: undefined,
}),
range_state: "provider_unavailable",
message:
@@ -337,8 +337,8 @@ describe("PredictionSuggestion", () => {
end_ts: suggestion.block_end,
label: suggestion.suggested,
provenance: "suggestion",
- user_id: null,
- confidence: null,
+ user_id: undefined,
+ confidence: undefined,
extend_forward: false,
});
@@ -392,8 +392,8 @@ describe("PredictionSuggestion", () => {
end_ts: suggestion.block_end,
label: suggestion.suggested,
provenance: "suggestion",
- user_id: null,
- confidence: null,
+ user_id: undefined,
+ confidence: undefined,
extend_forward: false,
});
@@ -427,8 +427,8 @@ describe("PredictionSuggestion", () => {
end_ts: suggestion.block_end,
label: "Debug",
provenance: "suggestion",
- user_id: null,
- confidence: null,
+ user_id: undefined,
+ confidence: undefined,
extend_forward: false,
});
@@ -464,8 +464,8 @@ describe("PredictionSuggestion", () => {
end_ts: suggestion.block_end,
label: "Debug",
provenance: "suggestion",
- user_id: null,
- confidence: null,
+ user_id: undefined,
+ confidence: undefined,
extend_forward: false,
});
diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
index 36b8254..29ba565 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
@@ -65,20 +65,18 @@ function suggestion_range_format(block_start: string, block_end: string): string
}
export const PredictionSuggestion: Component<{
- suggestion: Accessor;
+ suggestion: Accessor;
suggestions?: Accessor;
on_saved?: () => void;
- on_dismiss?: (
- reason?: SuggestionClearReason,
- suggestion?: LabelSuggestion | null,
- ) => void;
+ on_dismiss?: (reason?: SuggestionClearReason, suggestion?: LabelSuggestion) => void;
on_select?: (suggestion: LabelSuggestion) => void;
}> = (props) => {
const s = () => props.suggestion();
- const [error, set_error] = createSignal(null);
+ const [error, set_error] = createSignal(undefined);
const [busy, set_busy] = createSignal(false);
- const [overwrite_pending, set_overwrite_pending] =
- createSignal(null);
+ const [overwrite_pending, set_overwrite_pending] = createSignal<
+ OverwritePending | undefined
+ >(undefined);
const [change_label_open, set_change_label_open] = createSignal(false);
const [selected_label, set_selected_label] = createSignal("");
const [labels] = createResource(core_labels_list);
@@ -105,7 +103,7 @@ export const PredictionSuggestion: Component<{
const active_key = () => {
const sg = s();
if (!sg) {
- return null;
+ return undefined;
}
return label_suggestion_key(sg);
};
@@ -138,10 +136,7 @@ export const PredictionSuggestion: Component<{
}
}
- function notify_dismiss(
- reason: SuggestionClearReason,
- suggestion: LabelSuggestion | null,
- ) {
+ function notify_dismiss(reason: SuggestionClearReason, suggestion?: LabelSuggestion) {
if (props.on_dismiss) {
props.on_dismiss(reason, suggestion);
}
@@ -154,10 +149,10 @@ export const PredictionSuggestion: Component<{
}
function reset_for_suggestion(sg: LabelSuggestion) {
- set_overwrite_pending(null);
+ set_overwrite_pending(undefined);
set_change_label_open(false);
set_selected_label(sg.suggested);
- set_error(null);
+ set_error(undefined);
}
createEffect(
@@ -186,7 +181,7 @@ export const PredictionSuggestion: Component<{
return;
}
set_busy(true);
- set_error(null);
+ set_error(undefined);
try {
const payload = {
block_start: sg.block_start,
@@ -195,7 +190,7 @@ export const PredictionSuggestion: Component<{
...(sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
};
await notification_accept(payload);
- set_overwrite_pending(null);
+ set_overwrite_pending(undefined);
set_change_label_open(false);
notify_saved();
notify_dismiss("label_saved", sg);
@@ -233,7 +228,7 @@ export const PredictionSuggestion: Component<{
return;
}
set_busy(true);
- set_error(null);
+ set_error(undefined);
try {
const sg = s();
const payload = {
@@ -244,7 +239,7 @@ export const PredictionSuggestion: Component<{
...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
};
await notification_accept(payload);
- set_overwrite_pending(null);
+ set_overwrite_pending(undefined);
notify_saved();
notify_dismiss("label_saved", sg);
} catch (err: unknown) {
@@ -262,7 +257,7 @@ export const PredictionSuggestion: Component<{
return;
}
set_busy(true);
- set_error(null);
+ set_error(undefined);
try {
const sg = s();
const payload = {
@@ -273,7 +268,7 @@ export const PredictionSuggestion: Component<{
...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
};
await notification_accept(payload);
- set_overwrite_pending(null);
+ set_overwrite_pending(undefined);
notify_saved();
notify_dismiss("label_saved", sg);
} catch (err: unknown) {
@@ -291,14 +286,14 @@ export const PredictionSuggestion: Component<{
}
const sg = s();
set_busy(true);
- set_error(null);
+ set_error(undefined);
try {
if (sg && sg.suggestion_id) {
await notification_skip({ suggestion_id: sg.suggestion_id });
} else {
await notification_skip(undefined);
}
- set_overwrite_pending(null);
+ set_overwrite_pending(undefined);
set_change_label_open(false);
notify_dismiss("skipped", sg);
} catch (err: unknown) {
@@ -311,7 +306,7 @@ export const PredictionSuggestion: Component<{
}
function open_change_label(sg: LabelSuggestion) {
- set_error(null);
+ set_error(undefined);
set_selected_label(sg.suggested);
set_change_label_open(true);
}
@@ -327,7 +322,7 @@ export const PredictionSuggestion: Component<{
const suggestion_time_range = () => {
const sg = s();
if (!sg) {
- return null;
+ return undefined;
}
return { start: sg.block_start, end: sg.block_end };
};
@@ -685,13 +680,13 @@ export const PredictionSuggestion: Component<{
pending={pending()}
on_confirm={overwrite_confirm}
on_keep_all={keep_all_confirm}
- on_cancel={() => set_overwrite_pending(null)}
+ on_cancel={() => set_overwrite_pending(undefined)}
/>
)}
{(message) => (
- set_error(null)} />
+ set_error(undefined)} />
)}
diff --git a/src/taskclf/ui/frontend/src/components/StatusPanel.tsx b/src/taskclf/ui/frontend/src/components/StatusPanel.tsx
index edef34c..a022887 100644
--- a/src/taskclf/ui/frontend/src/components/StatusPanel.tsx
+++ b/src/taskclf/ui/frontend/src/components/StatusPanel.tsx
@@ -23,9 +23,9 @@ import { TrainingPanel } from "./TrainingPanel";
export const StatusPanel: Component<{
status: Accessor;
latest_status: Accessor;
- latest_prediction: Accessor;
+ latest_prediction: Accessor;
latest_tray_state: Accessor;
- active_suggestion: Accessor;
+ active_suggestion: Accessor;
pending_suggestions?: Accessor;
label_change_count?: Accessor;
ws_stats: Accessor;
diff --git a/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx b/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
index ca7d9df..52fc168 100644
--- a/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
+++ b/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
@@ -42,40 +42,43 @@ export const TrainingPanel: Component<{
);
const [synthetic, set_synthetic] = createSignal(false);
- const [data_check, set_data_check] = createSignal(null);
- const [checked_range, set_checked_range] = createSignal<{
- from: string;
- to: string;
- } | null>(null);
+ const [data_check, set_data_check] = createSignal(undefined);
+ const [checked_range, set_checked_range] = createSignal<
+ | {
+ from: string;
+ to: string;
+ }
+ | undefined
+ >(undefined);
const [models, set_models] = createSignal([]);
const [checking, set_checking] = createSignal(false);
- const [check_error, set_check_error] = createSignal(null);
- const [train_error, set_train_error] = createSignal(null);
+ const [check_error, set_check_error] = createSignal(undefined);
+ const [train_error, set_train_error] = createSignal(undefined);
const [submitting, set_submitting] = createSignal(false);
const [confirm_pending, set_confirm_pending] = createSignal(false);
const [dismissed_run_error_key, set_dismissed_run_error_key] = createSignal<
- string | null
- >(null);
+ string | undefined
+ >(undefined);
- const [expanded_bundle_id, set_expanded_bundle_id] = createSignal(
- null,
+ const [expanded_bundle_id, set_expanded_bundle_id] = createSignal(
+ undefined,
);
const [bundle_inspect_by_id, set_bundle_inspect_by_id] = createSignal<
Record
>({});
const [bundle_inspect_loading_id, set_bundle_inspect_loading_id] = createSignal<
- string | null
- >(null);
+ string | undefined
+ >(undefined);
const ts = () => props.train_state();
const is_running = () => ts().status === "running";
const run_error_key = () =>
- ts().error ? `${ts().job_id ?? "no-job"}:${ts().error}` : null;
+ ts().error ? `${ts().job_id ?? "no-job"}:${ts().error}` : undefined;
const visible_run_error = createMemo(() => {
const error = ts().error;
const key = run_error_key();
if (!error || key === dismissed_run_error_key()) {
- return null;
+ return undefined;
}
return error;
});
@@ -84,9 +87,9 @@ export const TrainingPanel: Component<{
on(
() => [date_from(), date_to()],
() => {
- set_data_check(null);
- set_checked_range(null);
- set_check_error(null);
+ set_data_check(undefined);
+ set_checked_range(undefined);
+ set_check_error(undefined);
},
{ defer: true },
),
@@ -107,8 +110,8 @@ export const TrainingPanel: Component<{
on(
run_error_key,
(key) => {
- if (key === null) {
- set_dismissed_run_error_key(null);
+ if (key === undefined) {
+ set_dismissed_run_error_key(undefined);
}
},
{ defer: true },
@@ -128,7 +131,7 @@ export const TrainingPanel: Component<{
const train_disabled_reason = createMemo(() => {
if (synthetic()) {
- return null;
+ return undefined;
}
const dc = data_check();
if (!dc) {
@@ -143,7 +146,7 @@ export const TrainingPanel: Component<{
if (dc.trainable_rows === 0) {
return "Labels don't overlap any feature windows — adjust labels or date range";
}
- return null;
+ return undefined;
});
function models_sorted(ml: ModelBundle[]) {
@@ -168,7 +171,7 @@ export const TrainingPanel: Component<{
async function toggle_bundle_inspect(model_id: string) {
if (expanded_bundle_id() === model_id) {
- set_expanded_bundle_id(null);
+ set_expanded_bundle_id(undefined);
return;
}
set_expanded_bundle_id(model_id);
@@ -186,7 +189,7 @@ export const TrainingPanel: Component<{
[model_id]: { error: msg },
}));
} finally {
- set_bundle_inspect_loading_id(null);
+ set_bundle_inspect_loading_id(undefined);
}
}
@@ -195,7 +198,7 @@ export const TrainingPanel: Component<{
return;
}
set_checking(true);
- set_check_error(null);
+ set_check_error(undefined);
try {
const [dc, ml] = await Promise.all([
training_data_check(date_from(), date_to()),
@@ -223,7 +226,7 @@ export const TrainingPanel: Component<{
}
set_confirm_pending(false);
set_submitting(true);
- set_train_error(null);
+ set_train_error(undefined);
try {
await training_start({
date_from: date_from(),
@@ -367,7 +370,7 @@ export const TrainingPanel: Component<{
set_check_error(null)}
+ on_close={() => set_check_error(undefined)}
/>
@@ -510,7 +513,7 @@ export const TrainingPanel: Component<{
set_train_error(null)}
+ on_close={() => set_train_error(undefined)}
/>
@@ -565,7 +568,7 @@ export const TrainingPanel: Component<{
tooltip="Latest progress message from the trainer"
/>
-
+
@@ -626,7 +629,7 @@ export const TrainingPanel: Component<{
mono
tooltip="Unique identifier for this model bundle"
/>
-
+
{
const v = s();
if (!v.candidate_app || !v.transition_threshold_s) {
- return null;
+ return undefined;
}
return Math.min(
100,
diff --git a/src/taskclf/ui/frontend/src/components/status/StatusActivityWatch.test.tsx b/src/taskclf/ui/frontend/src/components/status/StatusActivityWatch.test.tsx
index 4f4d258..92a8088 100644
--- a/src/taskclf/ui/frontend/src/components/status/StatusActivityWatch.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/status/StatusActivityWatch.test.tsx
@@ -10,8 +10,8 @@ describe("StatusActivityWatch", () => {
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,
@@ -24,7 +24,7 @@ describe("StatusActivityWatch", () => {
state: "setup_required",
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",
@@ -38,7 +38,7 @@ describe("StatusActivityWatch", () => {
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: {},
diff --git a/src/taskclf/ui/frontend/src/components/status/StatusModel.tsx b/src/taskclf/ui/frontend/src/components/status/StatusModel.tsx
index 18dca42..8a65920 100644
--- a/src/taskclf/ui/frontend/src/components/status/StatusModel.tsx
+++ b/src/taskclf/ui/frontend/src/components/status/StatusModel.tsx
@@ -22,27 +22,28 @@ export const StatusModel: Component<{
}> = (props) => {
const t = () => props.tray_state();
- const [bundle_inspect, set_bundle_inspect] =
- createSignal(null);
- const [bundle_inspect_error, set_bundle_inspect_error] = createSignal(
- null,
- );
+ const [bundle_inspect, set_bundle_inspect] = createSignal<
+ CurrentModelBundleInspectResponse | undefined
+ >(undefined);
+ const [bundle_inspect_error, set_bundle_inspect_error] = createSignal<
+ string | undefined
+ >(undefined);
createEffect(
on(
() => [t().model_loaded, t().model_dir] as const,
async ([loaded, dir]) => {
if (!loaded || !dir) {
- set_bundle_inspect(null);
- set_bundle_inspect_error(null);
+ set_bundle_inspect(undefined);
+ set_bundle_inspect_error(undefined);
return;
}
try {
const r = await model_bundle_inspect_current();
set_bundle_inspect(r);
- set_bundle_inspect_error(null);
+ set_bundle_inspect_error(undefined);
} catch (e: unknown) {
- set_bundle_inspect(null);
+ set_bundle_inspect(undefined);
set_bundle_inspect_error(
e instanceof Error ? e.message : "Bundle inspect failed",
);
diff --git a/src/taskclf/ui/frontend/src/components/status/StatusPrediction.tsx b/src/taskclf/ui/frontend/src/components/status/StatusPrediction.tsx
index 24d61dd..20c693c 100644
--- a/src/taskclf/ui/frontend/src/components/status/StatusPrediction.tsx
+++ b/src/taskclf/ui/frontend/src/components/status/StatusPrediction.tsx
@@ -6,7 +6,7 @@ import { StatusRow } from "../ui/StatusRow";
import { StatusSection } from "../ui/StatusSection";
export const StatusPrediction: Component<{
- prediction: Accessor;
+ prediction: Accessor;
}> = (props) => {
const pred = () => props.prediction();
diff --git a/src/taskclf/ui/frontend/src/components/status/StatusSuggestion.tsx b/src/taskclf/ui/frontend/src/components/status/StatusSuggestion.tsx
index 2c7457e..33f03fd 100644
--- a/src/taskclf/ui/frontend/src/components/status/StatusSuggestion.tsx
+++ b/src/taskclf/ui/frontend/src/components/status/StatusSuggestion.tsx
@@ -6,7 +6,7 @@ import { StatusRow } from "../ui/StatusRow";
import { StatusSection } from "../ui/StatusSection";
export const StatusSuggestion: Component<{
- suggestion: Accessor;
+ suggestion: Accessor;
pending_count?: Accessor;
}> = (props) => {
const sug = () => props.suggestion();
diff --git a/src/taskclf/ui/frontend/src/lib/api.ts b/src/taskclf/ui/frontend/src/lib/api.ts
index a2a8177..295b43c 100644
--- a/src/taskclf/ui/frontend/src/lib/api.ts
+++ b/src/taskclf/ui/frontend/src/lib/api.ts
@@ -1,3 +1,5 @@
+import { null_to_undefined } from "./nullish";
+
const BASE = "/api";
export type LabelResponse = {
@@ -5,8 +7,8 @@ export type LabelResponse = {
end_ts: string;
label: string;
provenance: string;
- user_id: string | null;
- confidence: number | null;
+ user_id: string | undefined;
+ confidence: number | undefined;
extend_forward: boolean;
};
@@ -16,16 +18,16 @@ export type QueueItem = {
bucket_start_ts: string;
bucket_end_ts: string;
reason: string;
- confidence: number | null;
- predicted_label: string | null;
+ confidence: number | undefined;
+ predicted_label: string | undefined;
status: string;
};
export type FeatureSummary = {
top_apps: { app_id: string; buckets: number }[];
- mean_keys_per_min: number | null;
- mean_clicks_per_min: number | null;
- mean_scroll_per_min: number | null;
+ mean_keys_per_min: number | undefined;
+ mean_clicks_per_min: number | undefined;
+ mean_scroll_per_min: number | undefined;
total_buckets: number;
session_count: number;
};
@@ -36,7 +38,7 @@ export type ActivityProviderStatus = {
state: "checking" | "ready" | "setup_required";
summary_available: boolean;
endpoint: string;
- source_id: string | null;
+ source_id: string | undefined;
last_sample_count: number;
last_sample_breakdown: Record;
setup_title: string;
@@ -54,7 +56,7 @@ export type ActivitySummary = FeatureSummary & {
activity_provider: ActivityProviderStatus;
recent_apps: AWLiveEntry[];
range_state: "ok" | "no_data" | "provider_unavailable";
- message: string | null;
+ message: string | undefined;
};
async function api_json(url: string, init?: RequestInit): Promise {
@@ -63,14 +65,14 @@ async function api_json(url: string, init?: RequestInit): Promise {
const text = await res.text().catch(() => "");
throw new Error(`${res.status}: ${text}`);
}
- return res.json();
+ return null_to_undefined(await res.json());
}
export async function labels_list(limit = 50): Promise {
return api_json(`${BASE}/labels?limit=${limit}`);
}
-export async function current_label_get(): Promise {
+export async function current_label_get(): Promise {
return api_json(`${BASE}/labels/current`);
}
@@ -220,26 +222,26 @@ export async function user_config_update(patch: {
// -- Training ----------------------------------------------------------------
export type TrainStatus = {
- job_id: string | null;
+ job_id: string | undefined;
status: "idle" | "running" | "complete" | "failed";
- step: string | null;
- progress_pct: number | null;
- message: string | null;
- error: string | null;
- metrics: Record | null;
- model_dir: string | null;
- started_at: string | null;
- finished_at: string | null;
+ step: string | undefined;
+ progress_pct: number | undefined;
+ message: string | undefined;
+ error: string | undefined;
+ metrics: Record | undefined;
+ model_dir: string | undefined;
+ started_at: string | undefined;
+ finished_at: string | undefined;
};
export type ModelBundle = {
model_id: string;
path: string;
valid: boolean;
- invalid_reason: string | null;
- macro_f1: number | null;
- weighted_f1: number | null;
- created_at: string | null;
+ invalid_reason: string | undefined;
+ macro_f1: number | undefined;
+ weighted_f1: number | undefined;
+ created_at: string | undefined;
};
export type DataCheck = {
diff --git a/src/taskclf/ui/frontend/src/lib/date.test.ts b/src/taskclf/ui/frontend/src/lib/date.test.ts
index 3318394..fc57af4 100644
--- a/src/taskclf/ui/frontend/src/lib/date.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/date.test.ts
@@ -5,7 +5,7 @@ describe("gap_shortcut_label_from_end", () => {
it("returns null when under one rounded minute", () => {
const end = Date.parse("2026-04-05T10:00:00.000Z");
const now = end + 29_000;
- expect(gap_shortcut_label_from_end(end, now)).toBeNull();
+ expect(gap_shortcut_label_from_end(end, now)).toBeUndefined();
});
it("formats minutes and hours like the gap button", () => {
diff --git a/src/taskclf/ui/frontend/src/lib/date.ts b/src/taskclf/ui/frontend/src/lib/date.ts
index 9f8a199..b38a1b3 100644
--- a/src/taskclf/ui/frontend/src/lib/date.ts
+++ b/src/taskclf/ui/frontend/src/lib/date.ts
@@ -84,16 +84,16 @@ export function time_input_date(dateStr: string, timeVal: string): Date {
}
/**
- * Quick-label gap shortcut text from a label's end time, or `null` when the
+ * Quick-label gap shortcut text from a label's end time, or `undefined` when the
* control should stay hidden (under one rounded minute since end).
*/
export function gap_shortcut_label_from_end(
end_ms: number,
now_ms: number,
-): string | null {
+): string | undefined {
const ago = Math.round((now_ms - end_ms) / 60_000);
if (ago < 1) {
- return null;
+ return undefined;
}
if (ago >= 60) {
return `gap ${Math.floor(ago / 60)}h${ago % 60 ? `${ago % 60}m` : ""}`;
@@ -123,9 +123,9 @@ export type TimeRange = {
end: string;
};
-export function time_range_minutes(mins: number): TimeRange | null {
+export function time_range_minutes(mins: number): TimeRange | undefined {
if (mins < 1) {
- return null;
+ return undefined;
}
const now = new Date();
const start = new Date(now.getTime() - mins * 60_000);
diff --git a/src/taskclf/ui/frontend/src/lib/format.ts b/src/taskclf/ui/frontend/src/lib/format.ts
index c42b647..518e820 100644
--- a/src/taskclf/ui/frontend/src/lib/format.ts
+++ b/src/taskclf/ui/frontend/src/lib/format.ts
@@ -12,7 +12,7 @@ export function duration_format(seconds: number): string {
return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
}
-export function time_format(iso: string | null | undefined): string {
+export function time_format(iso: string | undefined | undefined): string {
if (!iso) {
return "—";
}
@@ -28,7 +28,7 @@ export function time_format(iso: string | null | undefined): string {
}
}
-export function path_trunc(p: string | null | undefined, maxLen = 30): string {
+export function path_trunc(p: string | undefined | undefined, maxLen = 30): string {
if (!p) {
return "—";
}
@@ -43,9 +43,9 @@ export function app_name_short(app: string): string {
return parts[parts.length - 1];
}
-export function rate_fmt(v: number | null): string | null {
- if (v == null) {
- return null;
+export function rate_fmt(v: number | undefined): string | undefined {
+ if (v === undefined) {
+ return undefined;
}
return v < 10 ? v.toFixed(1) : String(Math.round(v));
}
diff --git a/src/taskclf/ui/frontend/src/lib/host.ts b/src/taskclf/ui/frontend/src/lib/host.ts
index e0580d1..130961a 100644
--- a/src/taskclf/ui/frontend/src/lib/host.ts
+++ b/src/taskclf/ui/frontend/src/lib/host.ts
@@ -63,11 +63,11 @@ declare global {
}
function electron_api_ref() {
- return window.electronHost ?? null;
+ return window.electronHost ?? undefined;
}
function pywebview_api_ref() {
- return window.pywebview?.api ?? null;
+ return window.pywebview?.api ?? undefined;
}
/**
@@ -78,10 +78,10 @@ function pywebview_api_ref() {
*/
class AdaptiveHost implements Host {
get kind(): HostKind {
- if (electron_api_ref() !== null) {
+ if (electron_api_ref() !== undefined) {
return "electron";
}
- if (pywebview_api_ref() !== null) {
+ if (pywebview_api_ref() !== undefined) {
return "pywebview";
}
return "browser";
diff --git a/src/taskclf/ui/frontend/src/lib/labelTimeline.ts b/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
index 234858e..e946613 100644
--- a/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
+++ b/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
@@ -13,7 +13,7 @@ export type OpenEndedLabelLike = Pick<
>;
export type TimelineSegment = {
- label: string | null;
+ label: string | undefined;
start_ms: number;
end_ms: number;
fraction: number;
@@ -51,7 +51,7 @@ export function day_timeline_build(
if (!entries.length) {
const seg: TimelineSegment = {
- label: null,
+ label: undefined,
start_ms: day_start,
end_ms: day_end,
fraction: 1,
@@ -83,7 +83,7 @@ export function day_timeline_build(
if (s > cursor) {
segments.push({
- label: null,
+ label: undefined,
start_ms: cursor,
end_ms: s,
fraction: (s - cursor) / span_ms,
@@ -118,7 +118,7 @@ export function day_timeline_build(
if (cursor < day_end) {
segments.push({
- label: null,
+ label: undefined,
start_ms: cursor,
end_ms: day_end,
fraction: (day_end - cursor) / span_ms,
diff --git a/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.test.ts b/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.test.ts
index 82c9cca..a1938c9 100644
--- a/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.test.ts
@@ -31,7 +31,7 @@ function selection_base(overrides: Partial = {}): TimeSelection {
return {
selected_minutes: 5,
fill_from_last: false,
- last_label_end_ts: null,
+ last_label_end_ts: undefined,
extend_fwd: true,
...overrides,
};
@@ -47,7 +47,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 5 }),
now,
);
- expect(result).toBeNull();
+ expect(result).toBeUndefined();
});
it("preserves conflicts that still overlap after time change", () => {
@@ -59,7 +59,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 15 }),
now,
);
- expect(result).not.toBeNull();
+ expect(result).not.toBeUndefined();
expect(result?.conflicts).toHaveLength(1);
expect(result?.conflicts[0].label).toBe("Communicate");
});
@@ -78,7 +78,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 5 }),
now,
);
- expect(result).not.toBeNull();
+ expect(result).not.toBeUndefined();
expect(result?.conflicts).toHaveLength(1);
expect(result?.conflicts[0].label).toBe("Review");
});
@@ -93,7 +93,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 10 }),
now,
);
- if (result === null) {
+ if (result === undefined) {
throw new Error("expected non-null result");
}
expect(new Date(result.start).getUTCMinutes()).toBe(20);
@@ -110,7 +110,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ fill_from_last: true, last_label_end_ts: iso(10, 45) }),
now,
);
- if (result === null) {
+ if (result === undefined) {
throw new Error("expected non-null result");
}
expect(new Date(result.start).getUTCMinutes()).toBe(45);
@@ -126,7 +126,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ fill_from_last: true, last_label_end_ts: iso(10, 0) }),
now,
);
- expect(result).toBeNull();
+ expect(result).toBeUndefined();
});
it("forces extend_forward when selected_minutes is 0", () => {
@@ -142,7 +142,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 0, extend_fwd: false }),
now,
);
- expect(result).not.toBeNull();
+ expect(result).not.toBeUndefined();
expect(result?.extend_forward).toBe(true);
});
@@ -156,7 +156,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 5, extend_fwd: false }),
now,
);
- expect(result).not.toBeNull();
+ expect(result).not.toBeUndefined();
expect(result?.extend_forward).toBe(false);
});
@@ -172,7 +172,7 @@ describe("label_overwrite_pending_upd_get", () => {
selection_base({ selected_minutes: 5 }),
now,
);
- expect(result).not.toBeNull();
+ expect(result).not.toBeUndefined();
expect(result?.label).toBe("Communicate");
expect(result?.confidence).toBe(0.8);
});
diff --git a/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.ts b/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.ts
index b0968b7..c2d3868 100644
--- a/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.ts
+++ b/src/taskclf/ui/frontend/src/lib/label_overwrite_pending_upd_get.ts
@@ -4,13 +4,13 @@ import { iso_date_parse } from "./date";
export type TimeSelection = {
selected_minutes: number;
fill_from_last: boolean;
- last_label_end_ts: string | null;
+ last_label_end_ts: string | undefined;
extend_fwd: boolean;
};
/**
* Recalculate an overwrite-pending state after the user changes the time
- * picker. Returns the updated pending, or `null` if no conflicts remain.
+ * picker. Returns the updated pending, or `undefined` if no conflicts remain.
*
* `now` is the current wall-clock time — callers should pass `new Date()`.
*/
@@ -18,7 +18,7 @@ export function label_overwrite_pending_upd_get(
pending: OverwritePending,
sel: TimeSelection,
now: Date,
-): OverwritePending | null {
+): OverwritePending | undefined {
let start: Date;
if (sel.fill_from_last && sel.last_label_end_ts) {
start = iso_date_parse(sel.last_label_end_ts);
@@ -37,7 +37,7 @@ export function label_overwrite_pending_upd_get(
});
if (remaining.length === 0) {
- return null;
+ return undefined;
}
return {
diff --git a/src/taskclf/ui/frontend/src/lib/notifications.test.ts b/src/taskclf/ui/frontend/src/lib/notifications.test.ts
index 8dfc7f1..7fd4fc6 100644
--- a/src/taskclf/ui/frontend/src/lib/notifications.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/notifications.test.ts
@@ -61,10 +61,10 @@ describe("transition_notification_show", () => {
const notification_close = vi.fn();
const notification_ctor = vi.fn(function NotificationMock(this: {
close: typeof notification_close;
- onclick: (() => void) | null;
+ onclick: (() => void) | undefined;
}) {
this.close = notification_close;
- this.onclick = null;
+ this.onclick = undefined;
});
Object.assign(notification_ctor, {
@@ -87,7 +87,7 @@ describe("transition_notification_show", () => {
renotify: true,
requireInteraction: true,
});
- expect(notification).not.toBeNull();
+ expect(notification).not.toBeUndefined();
notification?.onclick?.(new MouseEvent("click"));
@@ -109,7 +109,7 @@ describe("transition_notification_show", () => {
const { transition_notification_show } = await import("./notifications");
- expect(transition_notification_show(prompt, vi.fn())).toBeNull();
+ expect(transition_notification_show(prompt, vi.fn())).toBeUndefined();
expect(notification_ctor).not.toHaveBeenCalled();
});
@@ -118,8 +118,8 @@ describe("transition_notification_show", () => {
const no_suggestion_prompt: PromptLabelEvent = {
...prompt,
- suggested_label: null,
- suggestion_text: null,
+ suggested_label: undefined,
+ suggestion_text: undefined,
};
const notification_ctor = vi.fn(function NotificationMock() {});
Object.assign(notification_ctor, {
diff --git a/src/taskclf/ui/frontend/src/lib/notifications.ts b/src/taskclf/ui/frontend/src/lib/notifications.ts
index f088fc8..62caa1d 100644
--- a/src/taskclf/ui/frontend/src/lib/notifications.ts
+++ b/src/taskclf/ui/frontend/src/lib/notifications.ts
@@ -75,9 +75,9 @@ export async function notification_permission_ensure(): Promise {
export function transition_notification_show(
prompt: PromptLabelEvent,
on_click: () => void,
-): Notification | null {
+): Notification | undefined {
if (!permission_granted || !("Notification" in window)) {
- return null;
+ return undefined;
}
const range = notification_range_format(prompt);
diff --git a/src/taskclf/ui/frontend/src/lib/nullish.ts b/src/taskclf/ui/frontend/src/lib/nullish.ts
new file mode 100644
index 0000000..b51d7fb
--- /dev/null
+++ b/src/taskclf/ui/frontend/src/lib/nullish.ts
@@ -0,0 +1,24 @@
+/**
+ * Normalize wire-format JSON (`null`) into app-layer optional values (`undefined`).
+ * Use at fetch / WebSocket parse boundaries only.
+ */
+export function null_to_undefined(value: T): T {
+ if (value === null) {
+ return undefined as T;
+ }
+ if (Array.isArray(value)) {
+ return value.map((item) => null_to_undefined(item)) as T;
+ }
+ if (typeof value === "object") {
+ const record = value as Record;
+ const out: Record = {};
+ for (const key of Object.keys(record)) {
+ const v = record[key];
+ if (v !== null) {
+ out[key] = null_to_undefined(v);
+ }
+ }
+ return out as T;
+ }
+ return value;
+}
diff --git a/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.test.ts b/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.test.ts
index 35c0652..55354b9 100644
--- a/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.test.ts
@@ -11,7 +11,9 @@ describe("overwrite_pending_from_api_error", () => {
};
it("returns null when the message has no JSON", () => {
- expect(overwrite_pending_from_api_error(new Error("network"), params)).toBeNull();
+ expect(
+ overwrite_pending_from_api_error(new Error("network"), params),
+ ).toBeUndefined();
});
it("builds pending from structured detail.conflicting_spans", () => {
@@ -29,7 +31,7 @@ describe("overwrite_pending_from_api_error", () => {
};
const err = new Error(`409: ${JSON.stringify(body)}`);
const p = overwrite_pending_from_api_error(err, params);
- expect(p).not.toBeNull();
+ expect(p).not.toBeUndefined();
expect(p?.conflicts).toHaveLength(1);
expect(p?.conflicts[0].label).toBe("Build");
expect(p?.label).toBe("Write");
diff --git a/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.ts b/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.ts
index 71ceb46..aa40d1c 100644
--- a/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.ts
+++ b/src/taskclf/ui/frontend/src/lib/overwrite_pending_from_api_error.ts
@@ -12,11 +12,11 @@ export type OverwritePendingParams = {
export function overwrite_pending_from_api_error(
err: unknown,
params: OverwritePendingParams,
-): OverwritePending | null {
+): OverwritePending | undefined {
const msg = err instanceof Error ? err.message : "";
const json_match = msg.match(/\{[\s\S]*\}/);
if (!json_match) {
- return null;
+ return undefined;
}
try {
const parsed = JSON.parse(json_match[0]);
@@ -35,7 +35,7 @@ export function overwrite_pending_from_api_error(
});
}
if (spans.length === 0) {
- return null;
+ return undefined;
}
return {
label: params.label,
@@ -46,6 +46,6 @@ export function overwrite_pending_from_api_error(
extend_forward: params.extend_forward,
};
} catch {
- return null;
+ return undefined;
}
}
diff --git a/src/taskclf/ui/frontend/src/lib/transitionPromptNotifications.ts b/src/taskclf/ui/frontend/src/lib/transitionPromptNotifications.ts
index 73be93b..444dd96 100644
--- a/src/taskclf/ui/frontend/src/lib/transitionPromptNotifications.ts
+++ b/src/taskclf/ui/frontend/src/lib/transitionPromptNotifications.ts
@@ -45,7 +45,7 @@ function transition_prompt_clone(prompt: PromptLabelEvent): PromptLabelEvent {
}
export function transition_prompt_notifications_bind(
- prompt: Accessor,
+ prompt: Accessor,
on_open_label_grid: () => void,
): void {
onMount(() => {
diff --git a/src/taskclf/ui/frontend/src/lib/ws.test.ts b/src/taskclf/ui/frontend/src/lib/ws.test.ts
index d1b4f5f..8f60807 100644
--- a/src/taskclf/ui/frontend/src/lib/ws.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/ws.test.ts
@@ -5,10 +5,12 @@ import { suggestion_banner_ttl_ms_from_seconds, ws_store_new } from "./ws";
describe("suggestion_banner_ttl_ms_from_seconds", () => {
it("returns null when disabled or invalid", () => {
- expect(suggestion_banner_ttl_ms_from_seconds(0)).toBeNull();
- expect(suggestion_banner_ttl_ms_from_seconds(-1)).toBeNull();
- expect(suggestion_banner_ttl_ms_from_seconds(Number.NaN)).toBeNull();
- expect(suggestion_banner_ttl_ms_from_seconds(Number.POSITIVE_INFINITY)).toBeNull();
+ expect(suggestion_banner_ttl_ms_from_seconds(0)).toBeUndefined();
+ expect(suggestion_banner_ttl_ms_from_seconds(-1)).toBeUndefined();
+ expect(suggestion_banner_ttl_ms_from_seconds(Number.NaN)).toBeUndefined();
+ expect(
+ suggestion_banner_ttl_ms_from_seconds(Number.POSITIVE_INFINITY),
+ ).toBeUndefined();
});
it("returns milliseconds for positive seconds", () => {
@@ -27,10 +29,10 @@ class MockWebSocket {
readonly url: string;
readyState = MockWebSocket.CONNECTING;
- onopen: ((event: Event) => void) | null = null;
- onmessage: ((event: MessageEvent) => void) | null = null;
- onclose: ((event: CloseEvent) => void) | null = null;
- onerror: ((event: Event) => void) | null = null;
+ onopen: ((event: Event) => void) | undefined = undefined;
+ onmessage: ((event: MessageEvent) => void) | undefined = undefined;
+ onclose: ((event: CloseEvent) => void) | undefined = undefined;
+ onerror: ((event: Event) => void) | undefined = undefined;
constructor(url: string) {
this.url = url;
@@ -98,7 +100,7 @@ describe("ws_store_new badge display override", () => {
let store!: ReturnType;
const mounted = render(() => {
store = ws_store_new();
- return null;
+ return undefined;
});
unmounts.push(mounted.unmount);
return store;
@@ -151,7 +153,7 @@ describe("ws_store_new badge display override", () => {
reason: "skipped",
});
await waitFor(() => {
- expect(store.active_suggestion()).toBeNull();
+ expect(store.active_suggestion()).toBeUndefined();
});
expect(store.badge_display_override()).toEqual({
enabled: true,
@@ -168,7 +170,7 @@ describe("ws_store_new badge display override", () => {
await waitFor(() => {
expect(store.badge_display_override()).toEqual({
enabled: false,
- label: null,
+ label: undefined,
});
});
});
@@ -207,7 +209,7 @@ describe("ws_store_new badge display override", () => {
reason: "label_saved",
});
await waitFor(() => {
- expect(store.active_suggestion()).toBeNull();
+ expect(store.active_suggestion()).toBeUndefined();
});
expect(store.badge_display_override()).toEqual({
enabled: true,
@@ -223,7 +225,7 @@ describe("ws_store_new badge display override", () => {
await waitFor(() => {
expect(store.badge_display_override()).toEqual({
enabled: false,
- label: null,
+ label: undefined,
});
});
});
diff --git a/src/taskclf/ui/frontend/src/lib/ws.ts b/src/taskclf/ui/frontend/src/lib/ws.ts
index 6b4b058..8cc7e11 100644
--- a/src/taskclf/ui/frontend/src/lib/ws.ts
+++ b/src/taskclf/ui/frontend/src/lib/ws.ts
@@ -2,18 +2,21 @@ import { onCleanup, onMount } from "solid-js";
import { createStore, produce, reconcile } from "solid-js/store";
import { type ActivityProviderStatus, user_config_get } from "./api";
+import { null_to_undefined } from "./nullish";
/**
- * Maps persisted config seconds to a timer duration; `null` means no auto-dismiss.
+ * Maps persisted config seconds to a timer duration; `undefined` means no auto-dismiss.
* Exported for unit tests.
*/
-export function suggestion_banner_ttl_ms_from_seconds(seconds: number): number | null {
+export function suggestion_banner_ttl_ms_from_seconds(
+ seconds: number,
+): number | undefined {
if (!Number.isFinite(seconds) || seconds <= 0) {
- return null;
+ return undefined;
}
const ms = Math.floor(seconds) * 1000;
if (!Number.isFinite(ms) || ms <= 0) {
- return null;
+ return undefined;
}
return Math.min(ms, Number.MAX_SAFE_INTEGER);
}
@@ -67,8 +70,8 @@ export type StatusEvent = {
type: "status";
state: "idle" | "collecting" | "predicting" | "paused";
current_app: string;
- current_app_since: string | null;
- candidate_app: string | null;
+ current_app_since: string | undefined;
+ candidate_app: string | undefined;
candidate_duration_s: number;
transition_threshold_s: number;
poll_seconds: number;
@@ -77,7 +80,7 @@ export type StatusEvent = {
uptime_s: number;
activity_provider: ActivityProviderStatus;
aw_connected: boolean;
- aw_bucket_id: string | null;
+ aw_bucket_id: string | undefined;
aw_host: string;
last_event_count: number;
last_app_counts: Record;
@@ -86,8 +89,8 @@ const StatusEventDefault: StatusEvent = {
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,
@@ -100,7 +103,7 @@ const StatusEventDefault: StatusEvent = {
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",
@@ -114,7 +117,7 @@ const StatusEventDefault: StatusEvent = {
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: {},
@@ -131,12 +134,12 @@ export type TransitionInfo = {
export type TrayState = {
type: "tray_state";
model_loaded: boolean;
- model_dir: string | null;
- model_schema_hash: string | null;
- suggested_label: string | null;
- suggested_confidence: number | null;
+ model_dir: string | undefined;
+ model_schema_hash: string | undefined;
+ suggested_label: string | undefined;
+ suggested_confidence: number | undefined;
transition_count: number;
- last_transition: TransitionInfo | null;
+ last_transition: TransitionInfo | undefined;
labels_saved_count: number;
data_dir: string;
ui_port: number;
@@ -146,12 +149,12 @@ export type TrayState = {
const TrayStateDefault: TrayState = {
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,
@@ -170,8 +173,8 @@ export type PromptLabelEvent = {
block_start: string;
block_end: string;
duration_min: number;
- suggested_label: string | null;
- suggestion_text: string | null;
+ suggested_label: string | undefined;
+ suggestion_text: string | undefined;
};
export type SuggestionClearedEvent = {
@@ -226,15 +229,15 @@ export type TrainProgressEvent = {
type: "train_progress";
job_id: string;
step: string;
- progress_pct: number | null;
- message: string | null;
+ progress_pct: number | undefined;
+ message: string | undefined;
};
export type TrainCompleteEvent = {
type: "train_complete";
job_id: string;
- metrics: { macro_f1?: number; weighted_f1?: number } | null;
- model_dir: string | null;
+ metrics: { macro_f1?: number; weighted_f1?: number } | undefined;
+ model_dir: string | undefined;
};
export type TrainFailedEvent = {
@@ -264,14 +267,14 @@ export type WSEvent =
export type ConnectionStatus = "connecting" | "connected" | "disconnected";
export type TrainState = {
- job_id: string | null;
+ job_id: string | undefined;
status: "idle" | "running" | "complete" | "failed";
- step: string | null;
- progress_pct: number | null;
- message: string | null;
- error: string | null;
- metrics: { macro_f1?: number; weighted_f1?: number } | null;
- model_dir: string | null;
+ step: string | undefined;
+ progress_pct: number | undefined;
+ message: string | undefined;
+ error: string | undefined;
+ metrics: { macro_f1?: number; weighted_f1?: number } | undefined;
+ model_dir: string | undefined;
};
export type WSStats = {
@@ -280,26 +283,26 @@ export type WSStats = {
prediction_count: number;
tray_state_count: number;
suggestion_count: number;
- last_message_at: string | null;
+ last_message_at: string | undefined;
reconnect_count: number;
- connected_since: string | null;
+ connected_since: string | undefined;
};
export type BadgeDisplayOverride = {
enabled: boolean;
- label: string | null;
+ label: string | undefined;
};
export type WebSocketStore = {
latest_status: StatusEvent;
- latest_prediction: Prediction | null;
+ latest_prediction: Prediction | undefined;
latest_tray_state: TrayState;
- active_suggestion: LabelSuggestion | null;
+ active_suggestion: LabelSuggestion | undefined;
pending_suggestions: LabelSuggestion[];
badge_display_override: BadgeDisplayOverride;
- badge_display_restore_label: string | null;
- latest_prompt: PromptLabelEvent | null;
- live_status: LiveStatusEvent | null;
+ badge_display_restore_label: string | undefined;
+ latest_prompt: PromptLabelEvent | undefined;
+ live_status: LiveStatusEvent | undefined;
label_grid_requested: number;
label_change_count: number;
train_state: TrainState;
@@ -310,28 +313,28 @@ export type WebSocketStore = {
export function ws_store_new() {
const [store, setStore] = createStore({
latest_status: StatusEventDefault,
- latest_prediction: null,
+ latest_prediction: undefined,
latest_tray_state: TrayStateDefault,
- active_suggestion: null,
+ active_suggestion: undefined,
pending_suggestions: [],
badge_display_override: {
enabled: false,
- label: null,
+ label: undefined,
},
- badge_display_restore_label: null,
- latest_prompt: null,
- live_status: null,
+ badge_display_restore_label: undefined,
+ latest_prompt: undefined,
+ live_status: undefined,
label_grid_requested: 0,
label_change_count: 0,
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,
},
connection_status: "connecting",
ws_stats: {
@@ -340,17 +343,17 @@ export function ws_store_new() {
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,
},
});
let suggestion_ttl_seconds = 0;
- let ws: WebSocket | null = null;
- let reconnect_timer: ReturnType | null = null;
- let suggestion_timer: ReturnType | null = null;
+ let ws: WebSocket | undefined;
+ let reconnect_timer: ReturnType | undefined;
+ let suggestion_timer: ReturnType | undefined;
let retry_delay = 1000;
function badge_explicit_label_get(
@@ -360,7 +363,7 @@ export function ws_store_new() {
if (pred) {
return pred.mapped_label || pred.label;
}
- return state.live_status?.label ?? null;
+ return state.live_status?.label ?? undefined;
}
function badge_display_override_clear_if_superseded() {
@@ -370,8 +373,8 @@ export function ws_store_new() {
return;
}
state.badge_display_override.enabled = false;
- state.badge_display_override.label = null;
- state.badge_display_restore_label = null;
+ state.badge_display_override.label = undefined;
+ state.badge_display_restore_label = undefined;
}),
);
}
@@ -383,7 +386,7 @@ export function ws_store_new() {
}
if (
!state.badge_display_override.enabled
- || state.badge_display_restore_label == null
+ || state.badge_display_restore_label === undefined
) {
state.badge_display_restore_label = badge_explicit_label_get(state);
}
@@ -393,18 +396,18 @@ export function ws_store_new() {
function suggestion_active_set_in_state(
state: WebSocketStore,
- preferred_key?: string | null,
+ preferred_key?: string | undefined,
) {
const current_key = state.active_suggestion
? label_suggestion_key(state.active_suggestion)
- : null;
+ : undefined;
const key = preferred_key ?? current_key;
const next =
(key
? state.pending_suggestions.find((item) => label_suggestion_key(item) === key)
- : null)
+ : undefined)
?? state.pending_suggestions[0]
- ?? null;
+ ?? undefined;
state.active_suggestion = next;
suggestion_badge_override_apply_for_active(state);
@@ -416,7 +419,7 @@ export function ws_store_new() {
const suggestion_key = label_suggestion_key(suggestion);
const active_key = state.active_suggestion
? label_suggestion_key(state.active_suggestion)
- : null;
+ : undefined;
const existing_index = state.pending_suggestions.findIndex(
(item) => label_suggestion_key(item) === suggestion_key,
);
@@ -443,18 +446,20 @@ export function ws_store_new() {
function suggestion_queue_remove(
reason?: SuggestionClearReason,
- suggestion_id?: string | null,
+ suggestion_id?: string | undefined,
) {
setStore(
produce((state) => {
const active_key = state.active_suggestion
? label_suggestion_key(state.active_suggestion)
- : null;
+ : undefined;
const clear_key = suggestion_id ?? active_key;
const cleared_active =
- active_key != null && clear_key != null && active_key === clear_key;
+ active_key !== undefined
+ && clear_key !== undefined
+ && active_key === clear_key;
- if (clear_key != null) {
+ if (clear_key !== undefined) {
state.pending_suggestions = state.pending_suggestions.filter(
(item) => label_suggestion_key(item) !== clear_key,
);
@@ -463,19 +468,19 @@ export function ws_store_new() {
}
if (cleared_active) {
- state.active_suggestion = null;
+ state.active_suggestion = undefined;
}
suggestion_active_set_in_state(state);
if (state.active_suggestion) {
return;
}
- if (!state.badge_display_override.enabled || reason == null) {
+ if (!state.badge_display_override.enabled || reason === undefined) {
return;
}
if (reason === "skipped") {
state.badge_display_override.label = state.badge_display_restore_label;
}
- state.badge_display_restore_label = null;
+ state.badge_display_restore_label = undefined;
}),
);
}
@@ -502,7 +507,7 @@ export function ws_store_new() {
|| !Number.isFinite(stop_ms)
|| pred_ms <= stop_ms
) {
- state.latest_prediction = null;
+ state.latest_prediction = undefined;
}
}),
);
@@ -511,18 +516,18 @@ export function ws_store_new() {
function suggestion_timer_clear() {
if (suggestion_timer) {
clearTimeout(suggestion_timer);
- suggestion_timer = null;
+ suggestion_timer = undefined;
}
}
function suggestion_timer_start() {
suggestion_timer_clear();
const ttl_ms = suggestion_banner_ttl_ms_from_seconds(suggestion_ttl_seconds);
- if (ttl_ms == null) {
+ if (ttl_ms === undefined) {
return;
}
suggestion_timer = setTimeout(() => {
- suggestion_timer = null;
+ suggestion_timer = undefined;
suggestion_queue_remove();
}, ttl_ms);
}
@@ -554,12 +559,15 @@ export function ws_store_new() {
if (!resp.ok) {
return;
}
- const snap: Record = await resp.json();
+ const snap = null_to_undefined>(await resp.json());
if (snap.status) {
setStore("latest_status", reconcile(snap.status as StatusEvent));
}
if (snap.prediction) {
- setStore("latest_prediction", reconcile(snap.prediction as Prediction | null));
+ setStore(
+ "latest_prediction",
+ reconcile(snap.prediction as Prediction | undefined),
+ );
badge_display_override_clear_if_superseded();
}
if (snap.live_status) {
@@ -612,7 +620,7 @@ export function ws_store_new() {
ws.onmessage = (event) => {
try {
- const data: WSEvent = JSON.parse(event.data);
+ const data = null_to_undefined(JSON.parse(event.data));
const now = new Date().toISOString();
switch (data.type) {
case "status":
@@ -622,7 +630,7 @@ export function ws_store_new() {
});
break;
case "prediction":
- setStore("latest_prediction", reconcile(data as Prediction | null));
+ setStore("latest_prediction", reconcile(data as Prediction | undefined));
badge_display_override_clear_if_superseded();
ws_stats_bump(now, (s) => {
s.prediction_count++;
@@ -677,7 +685,7 @@ export function ws_store_new() {
ts: data.ts,
mapped_label: data.label,
provenance: "manual",
- } as Prediction | null),
+ } as Prediction | undefined),
);
badge_display_override_clear_if_superseded();
ws_stats_bump(now, (s) => {
@@ -713,8 +721,8 @@ export function ws_store_new() {
t.status = "complete";
t.step = "done";
t.progress_pct = 100;
- t.message = null;
- t.error = null;
+ t.message = undefined;
+ t.error = undefined;
t.metrics = data.metrics;
t.model_dir = data.model_dir;
}),
@@ -727,9 +735,9 @@ export function ws_store_new() {
produce((t) => {
t.job_id = data.job_id;
t.status = "failed";
- t.step = null;
- t.progress_pct = null;
- t.message = null;
+ t.step = undefined;
+ t.progress_pct = undefined;
+ t.message = undefined;
t.error = data.error;
}),
);
@@ -743,7 +751,7 @@ export function ws_store_new() {
ws.onclose = () => {
setStore("connection_status", "disconnected");
- setStore("ws_stats", "connected_since", null);
+ setStore("ws_stats", "connected_since", undefined);
reconnect_schedule();
};
@@ -762,7 +770,7 @@ export function ws_store_new() {
}
const jitter = retry_delay * (0.5 + Math.random() * 0.5);
reconnect_timer = setTimeout(() => {
- reconnect_timer = null;
+ reconnect_timer = undefined;
retry_delay = Math.min(retry_delay * 2, 30_000);
setStore("ws_stats", "reconnect_count", (c) => c + 1);
ws_connection_open();
@@ -773,7 +781,7 @@ export function ws_store_new() {
retry_delay = 1000;
if (reconnect_timer) {
clearTimeout(reconnect_timer);
- reconnect_timer = null;
+ reconnect_timer = undefined;
}
ws_connection_open();
}
@@ -786,7 +794,7 @@ export function ws_store_new() {
retry_delay = 1000;
if (reconnect_timer) {
clearTimeout(reconnect_timer);
- reconnect_timer = null;
+ reconnect_timer = undefined;
}
ws_connection_open();
}
@@ -794,11 +802,11 @@ export function ws_store_new() {
function suggestion_dismiss(
reason?: SuggestionClearReason,
- suggestion?: LabelSuggestion | null,
+ suggestion?: LabelSuggestion,
) {
suggestion_queue_remove(
reason,
- suggestion ? label_suggestion_key(suggestion) : null,
+ suggestion ? label_suggestion_key(suggestion) : undefined,
);
suggestion_timer_clear();
}
diff --git a/src/taskclf/ui/frontend/src/test/ws_store_stub.ts b/src/taskclf/ui/frontend/src/test/ws_store_stub.ts
index e49c881..172d85c 100644
--- a/src/taskclf/ui/frontend/src/test/ws_store_stub.ts
+++ b/src/taskclf/ui/frontend/src/test/ws_store_stub.ts
@@ -7,8 +7,8 @@ export function ws_store_stub() {
type: "status" as const,
state: "idle" as const,
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,
@@ -21,7 +21,7 @@ export function ws_store_stub() {
state: "checking" as const,
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",
@@ -35,35 +35,35 @@ export function ws_store_stub() {
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,
badge_display_override: () => ({
enabled: false,
- label: null,
+ label: undefined,
}),
latest_tray_state: () => ({
type: "tray_state" as const,
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,
pending_suggestions: () => [],
- latest_prompt: () => null,
- live_status: () => null,
+ latest_prompt: () => undefined,
+ live_status: () => undefined,
label_grid_requested: () => 0,
label_change_count: () => 0,
connection_status: () => "connected" as const,
@@ -73,19 +73,19 @@ export function ws_store_stub() {
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" as const,
- 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(),
suggestion_select: vi.fn(),
From 872f9614803fed17a44daccc418e6742fc07073b Mon Sep 17 00:00:00 2001
From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com>
Date: Wed, 20 May 2026 17:27:29 +1000
Subject: [PATCH 05/11] refactor: optional prop syntax to explicit or undefined
---
electron/launcher_choice.ts | 4 +-
electron/main.ts | 48 +++++-----
electron/node_http.ts | 10 +-
electron/port_conflict.ts | 4 +-
electron/update_policy.ts | 2 +-
electron/updater.ts | 91 +++++++++++--------
.../components/ActivitySourceSetupCallout.tsx | 2 +-
.../src/components/ActivitySummary.tsx | 8 +-
.../frontend/src/components/ConnectionDot.tsx | 8 +-
.../frontend/src/components/ErrorBanner.tsx | 2 +-
.../frontend/src/components/LabelHistory.tsx | 8 +-
.../src/components/LabelHistoryTimeline.tsx | 2 +-
.../ui/frontend/src/components/LabelLast.tsx | 8 +-
.../frontend/src/components/LabelRecorder.tsx | 38 +++++---
.../src/components/LabelTimePicker.tsx | 2 +-
.../src/components/PredictionBadge.tsx | 18 ++--
.../src/components/PredictionSuggestion.tsx | 43 ++++++---
.../frontend/src/components/StatusPanel.tsx | 6 +-
.../src/components/status/StatusPanelTab.tsx | 4 +-
.../components/status/StatusSuggestion.tsx | 2 +-
.../src/components/ui/StatusProgress.tsx | 4 +-
.../frontend/src/components/ui/StatusRow.tsx | 8 +-
.../src/components/ui/StatusSection.tsx | 6 +-
src/taskclf/ui/frontend/src/lib/api.ts | 45 ++++-----
src/taskclf/ui/frontend/src/lib/host.test.ts | 26 ++++--
src/taskclf/ui/frontend/src/lib/host.ts | 46 ++++++----
.../ui/frontend/src/lib/labelTimeline.ts | 4 +-
src/taskclf/ui/frontend/src/lib/log.ts | 2 +-
.../ui/frontend/src/lib/notifications.ts | 4 +-
src/taskclf/ui/frontend/src/lib/ws.ts | 31 ++++---
30 files changed, 283 insertions(+), 203 deletions(-)
diff --git a/electron/launcher_choice.ts b/electron/launcher_choice.ts
index ffdddaf..6c7e48c 100644
--- a/electron/launcher_choice.ts
+++ b/electron/launcher_choice.ts
@@ -2,8 +2,8 @@ import { compareVersions } from "./update_policy";
export interface LauncherReleaseEntry {
tag_name: string;
- draft?: boolean;
- prerelease?: boolean;
+ draft: boolean | undefined;
+ prerelease: boolean | undefined;
}
export function launcherVersionFromTag(tag: string): string | null {
diff --git a/electron/main.ts b/electron/main.ts
index 683042b..6d527b0 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -57,7 +57,9 @@ import { warmPillWindow } from "./shell_warm.js";
function readAppDisplayName(): string {
const pkgPath = path.join(__dirname, "..", "package.json");
const raw = fs.readFileSync(pkgPath, "utf-8");
- const pkg = JSON.parse(raw) as { build?: { productName?: string } };
+ const pkg = JSON.parse(raw) as {
+ build: { productName: string | undefined } | undefined;
+ };
return pkg.build?.productName ?? "taskclf";
}
@@ -65,9 +67,9 @@ const APP_DISPLAY_NAME = readAppDisplayName();
type HostCommand = {
cmd: string;
- mode?: string;
- message?: string;
- prompt?: {
+ mode: string | undefined;
+ message: string | undefined;
+ prompt: {
prev_app: string;
new_app: string;
block_start: string;
@@ -75,7 +77,7 @@ type HostCommand = {
duration_min: number;
suggested_label: string | null;
suggestion_text: string | null;
- };
+ } | undefined;
};
const COMPACT_SIZE = { width: 150, height: 30 };
@@ -293,7 +295,7 @@ function launcherLogFilePath(): string {
function launcherLog(
message: string,
level: "info" | "error" = "info",
- options?: { echoToConsole?: boolean },
+ options: { echoToConsole: boolean | undefined } | undefined = undefined,
): void {
const ts = new Date().toISOString();
const line = `[${ts}] [${level}] ${message}`;
@@ -364,7 +366,7 @@ function buildLauncherIssueUrl(title: string, detail: string): string {
return url;
}
-function fatalDialogDetail(message: string, detail?: string): string {
+function fatalDialogDetail(message: string, detail: string | undefined = undefined): string {
const parts: string[] = [message];
if (detail) {
parts.push(`Details:\n${detail}`);
@@ -379,7 +381,7 @@ function fatalDialogDetail(message: string, detail?: string): string {
async function showFatalLaunchError(
title: string,
message: string,
- detail?: string,
+ detail: string | undefined = undefined,
): Promise {
if (fatalLaunchErrorShown || isQuitting) {
return;
@@ -805,7 +807,7 @@ function transitionNotificationBody(prompt: NonNullable):
async function notificationActionPost(
pathName: string,
- body?: Record,
+ body: Record | undefined = undefined,
): Promise<{ ok: boolean; detail: string }> {
const response = await sidecarRequest(pathName, {
method: "POST",
@@ -1094,7 +1096,7 @@ async function waitForShell(url: string, timeoutMs = 30000): Promise {
async function sidecarRequest(
pathName: string,
- init?: RequestInit,
+ init: RequestInit | undefined = undefined,
): Promise {
try {
return await fetch(`http://127.0.0.1:${uiPort()}${pathName}`, init);
@@ -1170,10 +1172,10 @@ let updateCheckInProgress = false;
function payloadResolutionDetails(
resolution: PayloadResolution,
activeVersion: string | null,
- options?: {
- selectedVersion?: string | null;
- note?: string | null;
- },
+ options: {
+ selectedVersion: string | null | undefined;
+ note: string | null | undefined;
+ } | undefined = undefined,
): string {
const lines = [
`Launcher version: ${resolution.launcherManifest.launcher_version}`,
@@ -1213,9 +1215,9 @@ async function applyPayloadResolution(
async function applyPayloadResolutionAndRelaunch(
resolution: PayloadResolution,
heading: string,
- options?: {
- clearSelectedVersionBeforeRelaunch?: boolean;
- },
+ options: {
+ clearSelectedVersionBeforeRelaunch: boolean | undefined;
+ } | undefined = undefined,
): Promise {
await applyPayloadResolution(resolution, heading);
if (options?.clearSelectedVersionBeforeRelaunch) {
@@ -1225,7 +1227,7 @@ async function applyPayloadResolutionAndRelaunch(
app.quit();
}
-async function showUpdateCheckFailureDialog(detail?: string): Promise {
+async function showUpdateCheckFailureDialog(detail: string | undefined = undefined): Promise {
await dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
@@ -2262,11 +2264,11 @@ function formatProgressLine(event: UpdateProgressEvent): string {
}
}
-type ProgressWindowState = {
- heading?: string;
- detail?: string;
- percent?: number | null;
-};
+type ProgressWindowState = Partial<{
+ heading: string | undefined;
+ detail: string | undefined;
+ percent: number | null | undefined;
+}>;
function progressWindowPageHtml(heading: string, detail = ""): string {
const h = escapeHtmlAttr(heading);
diff --git a/electron/node_http.ts b/electron/node_http.ts
index 8b53919..7ac56b1 100644
--- a/electron/node_http.ts
+++ b/electron/node_http.ts
@@ -3,13 +3,13 @@ import https from "node:https";
import { Readable } from "node:stream";
export interface NodeFetchInit extends RequestInit {
- maxRedirects?: number;
+ maxRedirects: number | undefined;
}
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
const NULL_BODY_STATUS_CODES = new Set([204, 205, 304]);
-function abortError(reason?: unknown): Error {
+function abortError(reason: unknown | undefined = undefined): Error {
if (reason instanceof Error) {
return reason;
}
@@ -71,7 +71,7 @@ async function nodeFetchRequest(
remainingRedirects: number,
): Promise {
if (request.signal.aborted) {
- throw abortError((request.signal as AbortSignal & { reason?: unknown }).reason);
+ throw abortError((request.signal as AbortSignal & { reason: unknown | undefined }).reason);
}
const url = new URL(request.url);
@@ -170,7 +170,7 @@ async function nodeFetchRequest(
req.on("error", fail);
const onAbort = () => {
- const error = abortError((request.signal as AbortSignal & { reason?: unknown }).reason);
+ const error = abortError((request.signal as AbortSignal & { reason: unknown | undefined }).reason);
req.destroy(error);
fail(error);
};
@@ -191,7 +191,7 @@ async function nodeFetchRequest(
export async function nodeFetch(
input: string | URL | Request,
- init?: NodeFetchInit,
+ init: NodeFetchInit | undefined = undefined,
): Promise {
const request = input instanceof Request && init === undefined
? input
diff --git a/electron/port_conflict.ts b/electron/port_conflict.ts
index 733c30b..1932663 100644
--- a/electron/port_conflict.ts
+++ b/electron/port_conflict.ts
@@ -24,13 +24,13 @@ export type SpawnSyncResult = {
export type SpawnSyncFn = (
command: string,
args: readonly string[],
- options?: SpawnSyncOptionsWithStringEncoding,
+ options: SpawnSyncOptionsWithStringEncoding | undefined,
) => SpawnSyncResult;
export function defaultSpawnSync(
command: string,
args: readonly string[],
- options?: SpawnSyncOptionsWithStringEncoding,
+ options: SpawnSyncOptionsWithStringEncoding | undefined = undefined,
): SpawnSyncResult {
const r = spawnSync(command, args as string[], {
...options,
diff --git a/electron/update_policy.ts b/electron/update_policy.ts
index 8321730..bddf441 100644
--- a/electron/update_policy.ts
+++ b/electron/update_policy.ts
@@ -65,7 +65,7 @@ export function selectLatestCompatiblePayloadVersion(
export function manifestUrlForLauncherVersion(
version: string,
- overrideUrl?: string,
+ overrideUrl: string | undefined,
): string {
if (overrideUrl && overrideUrl.length > 0) {
return overrideUrl;
diff --git a/electron/updater.ts b/electron/updater.ts
index a319bf0..229bf5c 100644
--- a/electron/updater.ts
+++ b/electron/updater.ts
@@ -25,8 +25,8 @@ export interface PayloadPlatformData {
}
export interface PayloadManifest {
- kind?: "payload";
- schema_version?: number;
+ kind: "payload" | undefined;
+ schema_version: number | undefined;
version: string;
/** Keys are LLVM-style target triples, e.g. x86_64-unknown-linux-gnu */
platforms: Record;
@@ -35,16 +35,16 @@ export interface PayloadManifest {
export type Manifest = PayloadManifest;
export interface LauncherManifest {
- kind?: "launcher";
- schema_version?: number;
- version?: string;
+ kind: "launcher" | undefined;
+ schema_version: number | undefined;
+ version: string | undefined;
launcher_version: string;
payload_index_url: string;
- default_payload_selection?: {
+ default_payload_selection: {
strategy: "latest-compatible";
- };
+ } | undefined;
compatible_payloads: CompatiblePayloadRange;
- platforms?: Record;
+ platforms: Record | undefined;
}
export interface LauncherPlatformData {
@@ -59,7 +59,7 @@ export interface GitHubReleaseAsset {
}
export interface GitHubRelease extends LauncherReleaseEntry {
- assets?: GitHubReleaseAsset[];
+ assets: GitHubReleaseAsset[] | undefined;
}
export interface LauncherResolution {
@@ -77,9 +77,9 @@ export interface PayloadIndexEntry {
}
export interface PayloadIndex {
- kind?: "payload-index";
- schema_version?: number;
- generated_at?: string;
+ kind: "payload-index" | undefined;
+ schema_version: number | undefined;
+ generated_at: string | undefined;
payloads: PayloadIndexEntry[];
}
@@ -101,22 +101,22 @@ export type UpdatePhase = "download" | "verify" | "extract";
export interface UpdateProgressEvent {
phase: UpdatePhase;
/** Bytes received so far during download */
- receivedBytes?: number;
+ receivedBytes: number | undefined;
/** Total bytes when Content-Length is present */
- totalBytes?: number | null;
+ totalBytes: number | null | undefined;
/** 0–100 when known; null if total size is unknown */
- percent?: number | null;
+ percent: number | null | undefined;
}
export interface DownloadAndApplyOptions {
- onProgress?: (event: UpdateProgressEvent) => void | Promise;
+ onProgress: ((event: UpdateProgressEvent) => void | Promise) | undefined;
}
export interface CheckForUpdateOptions {
/** Abort manifest fetch after this many milliseconds; <= 0 disables the timeout. */
- timeoutMs?: number;
- preferredVersion?: string;
- ignoreSelectedVersion?: boolean;
+ timeoutMs: number | undefined;
+ preferredVersion: string | undefined;
+ ignoreSelectedVersion: boolean | undefined;
}
const GITHUB_LAUNCHER_RELEASES_API_URL = "https://api.github.com/repos/fruitiecutiepie/taskclf/releases?per_page=100";
@@ -146,7 +146,7 @@ function describeFetchError(error: unknown): string {
return String(error);
}
- const cause = (error as Error & { cause?: unknown }).cause;
+ const cause = (error as Error & { cause: unknown | undefined }).cause;
if (cause === undefined || cause === null) {
return error.message;
}
@@ -161,11 +161,12 @@ function describeFetchError(error: unknown): string {
async function updaterFetch(
input: string,
purpose: string,
- init?: NodeFetchInit,
+ init: NodeFetchInit | undefined = undefined,
): Promise {
try {
return await nodeFetch(input, {
cache: "no-store",
+ maxRedirects: init?.maxRedirects ?? undefined,
...init,
});
} catch (error) {
@@ -321,8 +322,8 @@ export let lastLauncherCheckFailure: string | null = null;
async function fetchWithTimeout(
input: string,
purpose: string,
- timeoutMs?: number,
- init?: NodeFetchInit,
+ timeoutMs: number | undefined = undefined,
+ init: NodeFetchInit | undefined = undefined,
): Promise {
if (timeoutMs === undefined || timeoutMs <= 0) {
return updaterFetch(input, purpose, init);
@@ -334,7 +335,11 @@ async function fetchWithTimeout(
}, timeoutMs);
try {
- return await updaterFetch(input, purpose, { ...init, signal: controller.signal });
+ return await updaterFetch(input, purpose, {
+ maxRedirects: init?.maxRedirects ?? undefined,
+ ...init,
+ signal: controller.signal,
+ });
} finally {
clearTimeout(timer);
}
@@ -343,8 +348,8 @@ async function fetchWithTimeout(
async function fetchJsonWithTimeout(
input: string,
purpose: string,
- timeoutMs?: number,
- init?: NodeFetchInit,
+ timeoutMs: number | undefined = undefined,
+ init: NodeFetchInit | undefined = undefined,
): Promise {
const res = await fetchWithTimeout(input, purpose, timeoutMs, init);
if (!res.ok) {
@@ -389,7 +394,7 @@ function launcherManifestUrlFromRelease(
if (assetUrl) {
return assetUrl;
}
- return manifestUrlForLauncherVersion(version);
+ return manifestUrlForLauncherVersion(version, undefined);
}
function validateLauncherPlatformData(
@@ -419,8 +424,8 @@ function findPayloadIndexEntry(payloadIndex: PayloadIndex, version: string): Pay
function resolveDesiredPayloadVersion(
launcherManifest: LauncherManifest,
payloadIndex: PayloadIndex,
- preferredVersion?: string,
- ignoreSelectedVersion?: boolean,
+ preferredVersion: string | undefined = undefined,
+ ignoreSelectedVersion: boolean | undefined = undefined,
): {
defaultVersion: string;
version: string;
@@ -478,7 +483,7 @@ function resolveDesiredPayloadVersion(
}
export async function resolvePayloadRelease(
- options?: CheckForUpdateOptions,
+ options: Partial | undefined = undefined,
): Promise {
const launcherManifestUrl = manifestUrlForLauncherVersion(
app.getVersion(),
@@ -554,7 +559,7 @@ export async function resolvePayloadRelease(
}
export async function resolveLauncherRelease(
- options?: CheckForUpdateOptions,
+ options: Partial | undefined = undefined,
): Promise {
const timeoutMs = options?.timeoutMs;
lastLauncherCheckFailure = null;
@@ -564,7 +569,7 @@ export async function resolveLauncherRelease(
GITHUB_LAUNCHER_RELEASES_API_URL,
"launcher releases",
timeoutMs,
- { headers: githubApiHeaders() },
+ { headers: githubApiHeaders(), maxRedirects: undefined },
);
const latestTag = selectLatestLauncherReleaseTag(releases);
if (latestTag === null) {
@@ -583,7 +588,7 @@ export async function resolveLauncherRelease(
launcherManifestUrlFromRelease(matchingRelease, latestVersion),
`launcher manifest for ${latestTag}`,
timeoutMs,
- { headers: githubApiHeaders() },
+ { headers: githubApiHeaders(), maxRedirects: undefined },
);
const effectiveLatestVersion = latestManifest.version ?? latestManifest.launcher_version ?? latestVersion;
const currentVersion = app.getVersion();
@@ -612,7 +617,7 @@ export async function resolveLauncherRelease(
}
export async function checkForUpdate(
- options?: CheckForUpdateOptions,
+ options: Partial | undefined = undefined,
): Promise {
const resolution = await resolvePayloadRelease(options);
if (resolution === null) {
@@ -701,7 +706,7 @@ async function streamPayloadToFile(
export async function downloadAndApplyUpdate(
manifest: PayloadManifest,
- options?: DownloadAndApplyOptions,
+ options: DownloadAndApplyOptions | undefined = undefined,
): Promise {
const onProgress = options?.onProgress;
try {
@@ -735,11 +740,21 @@ export async function downloadAndApplyUpdate(
// Verify Hash (already verified while streaming; emit phase for UI)
console.log(`[updater] Verifying hash...`);
- await emitProgress(onProgress, { phase: "verify", percent: 100 });
+ await emitProgress(onProgress, {
+ phase: "verify",
+ receivedBytes: undefined,
+ totalBytes: undefined,
+ percent: 100,
+ });
// Extract
console.log(`[updater] Extracting payload...`);
- await emitProgress(onProgress, { phase: "extract", percent: null });
+ await emitProgress(onProgress, {
+ phase: "extract",
+ receivedBytes: undefined,
+ totalBytes: undefined,
+ percent: null,
+ });
const zip = new AdmZip(zipPath);
zip.extractAllTo(payloadDir, true);
@@ -766,7 +781,7 @@ export async function downloadAndApplyUpdate(
export async function downloadLauncherInstaller(
resolution: LauncherResolution,
- options?: DownloadAndApplyOptions,
+ options: DownloadAndApplyOptions | undefined = undefined,
): Promise {
if (!resolution.updateAvailable) {
throw new Error(`Launcher v${resolution.currentVersion} is already current`);
diff --git a/src/taskclf/ui/frontend/src/components/ActivitySourceSetupCallout.tsx b/src/taskclf/ui/frontend/src/components/ActivitySourceSetupCallout.tsx
index 50478ca..842ba48 100644
--- a/src/taskclf/ui/frontend/src/components/ActivitySourceSetupCallout.tsx
+++ b/src/taskclf/ui/frontend/src/components/ActivitySourceSetupCallout.tsx
@@ -3,7 +3,7 @@ import type { ActivityProviderStatus } from "../lib/api";
export const ActivitySourceSetupCallout: Component<{
provider: ActivityProviderStatus;
- compact?: boolean;
+ compact: boolean | undefined;
}> = (props) => {
const compact = () => props.compact ?? false;
diff --git a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
index 34ab7b5..ea0c723 100644
--- a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
+++ b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
@@ -45,10 +45,10 @@ const PredictionBadge: Component<{ p: Accessor }> = (props) => (
);
export const ActivitySummary: Component<{
- minutes?: Accessor;
- time_range?: Accessor;
- prediction?: Accessor;
- show_empty?: boolean;
+ minutes: Accessor | undefined;
+ time_range: Accessor | undefined;
+ prediction: Accessor | undefined;
+ show_empty: boolean | undefined;
}> = (props) => {
const range = () =>
props.time_range?.()
diff --git a/src/taskclf/ui/frontend/src/components/ConnectionDot.tsx b/src/taskclf/ui/frontend/src/components/ConnectionDot.tsx
index a04a1a3..236c71a 100644
--- a/src/taskclf/ui/frontend/src/components/ConnectionDot.tsx
+++ b/src/taskclf/ui/frontend/src/components/ConnectionDot.tsx
@@ -4,10 +4,10 @@ import type { ConnectionStatus } from "../lib/ws";
export const ConnectionDot: Component<{
status: Accessor;
- panel_pinned?: Accessor;
- on_toggle_panel?: () => void;
- on_show_panel?: () => void;
- on_hide_panel?: () => void;
+ panel_pinned: Accessor | undefined;
+ on_toggle_panel: (() => void) | undefined;
+ on_show_panel: (() => void) | undefined;
+ on_hide_panel: (() => void) | undefined;
}> = (props) => {
const [hovered, set_hovered] = createSignal(false);
const color = () => dot_color(props.status());
diff --git a/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx b/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx
index 4255ee2..c70ae86 100644
--- a/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx
+++ b/src/taskclf/ui/frontend/src/components/ErrorBanner.tsx
@@ -31,7 +31,7 @@ async function error_text_copy(text: string): Promise {
export const ErrorBanner: Component<{
message: string;
- on_close?: () => void;
+ on_close: (() => void) | undefined;
}> = (props) => {
const [copy_state, set_copy_state] = createSignal<"idle" | "copied" | "failed">(
"idle",
diff --git a/src/taskclf/ui/frontend/src/components/LabelHistory.tsx b/src/taskclf/ui/frontend/src/components/LabelHistory.tsx
index aa57615..e2ebbc8 100644
--- a/src/taskclf/ui/frontend/src/components/LabelHistory.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelHistory.tsx
@@ -30,7 +30,7 @@ import { LabelHistoryTimeline } from "./LabelHistoryTimeline";
export const LabelHistory: Component<{
visible: Accessor;
- label_change_count?: Accessor;
+ label_change_count: Accessor | undefined;
}> = (props) => {
const [selected_date, set_selected_date] = createSignal(date_today_str());
const [known_today, set_known_today] = createSignal(selected_date());
@@ -124,6 +124,7 @@ export const LabelHistory: Component<{
label: new_label,
new_start_ts: new_start,
new_end_ts: new_end,
+ extend_forward: undefined,
});
set_flash(new_label);
setTimeout(() => {
@@ -165,6 +166,11 @@ export const LabelHistory: Component<{
start_ts,
end_ts,
label,
+ user_id: undefined,
+ confidence: undefined,
+ extend_forward: undefined,
+ overwrite: undefined,
+ allow_overlap: undefined,
});
set_flash(label);
setTimeout(() => {
diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx
index 76218d1..9351264 100644
--- a/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelHistoryTimeline.tsx
@@ -5,7 +5,7 @@ import type { TimelineSegment } from "../lib/labelTimeline";
export const LabelHistoryTimeline: Component<{
segments: TimelineSegment[];
- on_segment_click?: (seg: TimelineSegment, index: number) => void;
+ on_segment_click: ((seg: TimelineSegment, index: number) => void) | undefined;
}> = (props) => {
const [tooltip, set_tooltip] = createSignal<{ text: string; x: number } | undefined>(
undefined,
diff --git a/src/taskclf/ui/frontend/src/components/LabelLast.tsx b/src/taskclf/ui/frontend/src/components/LabelLast.tsx
index 7fabb93..9b2c818 100644
--- a/src/taskclf/ui/frontend/src/components/LabelLast.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelLast.tsx
@@ -9,12 +9,12 @@ type LabelLastProps = {
label: string;
start_ts: string;
end_ts: string;
- extend_forward?: boolean;
+ extend_forward: boolean | undefined;
}
| undefined
| undefined
>;
- is_current?: Accessor;
+ is_current: Accessor | undefined;
};
const LabelLastContent: Component<{
@@ -22,9 +22,9 @@ const LabelLastContent: Component<{
label: string;
start_ts: string;
end_ts: string;
- extend_forward?: boolean;
+ extend_forward: boolean | undefined;
}>;
- is_current?: Accessor;
+ is_current: Accessor | undefined;
}> = (props) => {
const is_current = () =>
props.is_current?.() ?? label_entry_is_open_ended(props.ll());
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx
index 4355212..202ff26 100644
--- a/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelRecorder.tsx
@@ -42,17 +42,19 @@ function extend_forward_pref_read(): boolean {
}
type LabelRecorderProps = {
- max_height?: number;
+ max_height: number | undefined;
on_collapse: () => void;
- prediction?: Accessor;
- suggestion?: Accessor;
- suggestions?: Accessor;
- label_change_count?: Accessor;
- on_suggestion_dismiss?: (
- reason?: SuggestionClearReason,
- suggestion?: LabelSuggestion,
- ) => void;
- on_suggestion_select?: (suggestion: LabelSuggestion) => void;
+ prediction: Accessor | undefined;
+ suggestion: Accessor | undefined;
+ suggestions: Accessor | undefined;
+ label_change_count: Accessor | undefined;
+ on_suggestion_dismiss:
+ | ((
+ reason: SuggestionClearReason | undefined,
+ suggestion: LabelSuggestion | undefined,
+ ) => void)
+ | undefined;
+ on_suggestion_select: ((suggestion: LabelSuggestion) => void) | undefined;
};
export const LabelRecorder: Component = (props) => {
@@ -162,8 +164,11 @@ export const LabelRecorder: Component = (props) => {
start_ts: start.toISOString(),
end_ts: now.toISOString(),
label,
+ user_id: undefined,
confidence: conf_percent() / 100,
extend_forward: effective_extend,
+ overwrite: undefined,
+ allow_overlap: undefined,
});
set_flash(label);
set_label_version((v) => v + 1);
@@ -197,9 +202,11 @@ export const LabelRecorder: Component = (props) => {
start_ts: pending.start,
end_ts: pending.end,
label: pending.label,
+ user_id: undefined,
confidence: pending.confidence,
extend_forward: pending.extend_forward,
overwrite: true,
+ allow_overlap: undefined,
});
set_flash(pending.label);
set_label_version((v) => v + 1);
@@ -221,8 +228,10 @@ export const LabelRecorder: Component = (props) => {
start_ts: pending.start,
end_ts: pending.end,
label: pending.label,
+ user_id: undefined,
confidence: pending.confidence,
extend_forward: pending.extend_forward,
+ overwrite: undefined,
allow_overlap: true,
});
set_flash(pending.label);
@@ -250,6 +259,7 @@ export const LabelRecorder: Component = (props) => {
start_ts: current.start_ts,
end_ts: current.end_ts,
label: current.label,
+ new_start_ts: undefined,
new_end_ts: stop_ts,
extend_forward: false,
});
@@ -291,8 +301,12 @@ export const LabelRecorder: Component = (props) => {
last_label={last_ended_label}
/>
-
-
+
diff --git a/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx b/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
index 2046e88..85c5a38 100644
--- a/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelTimePicker.tsx
@@ -25,7 +25,7 @@ type LabelTimePickerProps = {
set_fill_from_last: (v: boolean) => void;
has_current_label: Accessor;
last_label: Accessor<
- | { start_ts: string; end_ts: string; extend_forward?: boolean }
+ | { start_ts: string; end_ts: string; extend_forward: boolean | undefined }
| undefined
| undefined
>;
diff --git a/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx b/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
index 6c520a7..30ee0f4 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionBadge.tsx
@@ -16,17 +16,17 @@ export const PredictionBadge: Component<{
latest_status: Accessor;
latest_prediction: Accessor;
live_status: Accessor;
- badge_display_override?: Accessor;
+ badge_display_override: Accessor | undefined;
latest_tray_state: Accessor;
active_suggestion: Accessor;
- label_pinned?: Accessor;
- panel_pinned?: Accessor;
- on_toggle_panel?: () => void;
- on_show_panel?: () => void;
- on_hide_panel?: () => void;
- on_toggle_label?: () => void;
- on_show_label?: () => void;
- on_hide_label?: () => void;
+ label_pinned: Accessor | undefined;
+ panel_pinned: Accessor | undefined;
+ on_toggle_panel: (() => void) | undefined;
+ on_show_panel: (() => void) | undefined;
+ on_hide_panel: (() => void) | undefined;
+ on_toggle_label: (() => void) | undefined;
+ on_show_label: (() => void) | undefined;
+ on_hide_label: (() => void) | undefined;
}> = (props) => {
const prediction_label = () => {
const pred = props.latest_prediction();
diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
index 29ba565..e41013e 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.tsx
@@ -66,10 +66,15 @@ function suggestion_range_format(block_start: string, block_end: string): string
export const PredictionSuggestion: Component<{
suggestion: Accessor;
- suggestions?: Accessor;
- on_saved?: () => void;
- on_dismiss?: (reason?: SuggestionClearReason, suggestion?: LabelSuggestion) => void;
- on_select?: (suggestion: LabelSuggestion) => void;
+ suggestions: Accessor | undefined;
+ on_saved: (() => void) | undefined;
+ on_dismiss:
+ | ((
+ reason: SuggestionClearReason | undefined,
+ suggestion: LabelSuggestion | undefined,
+ ) => void)
+ | undefined;
+ on_select: ((suggestion: LabelSuggestion) => void) | undefined;
}> = (props) => {
const s = () => props.suggestion();
const [error, set_error] = createSignal(undefined);
@@ -90,7 +95,7 @@ export const PredictionSuggestion: Component<{
};
const pending_suggestions = () => {
- if (props.suggestions) {
+ if (props.suggestions !== undefined) {
return props.suggestions();
}
const sg = s();
@@ -131,19 +136,22 @@ export const PredictionSuggestion: Component<{
const correction_label = () => selected_label();
function notify_saved() {
- if (props.on_saved) {
+ if (props.on_saved !== undefined) {
props.on_saved();
}
}
- function notify_dismiss(reason: SuggestionClearReason, suggestion?: LabelSuggestion) {
- if (props.on_dismiss) {
+ function notify_dismiss(
+ reason: SuggestionClearReason,
+ suggestion: LabelSuggestion | undefined,
+ ) {
+ if (props.on_dismiss !== undefined) {
props.on_dismiss(reason, suggestion);
}
}
function select_suggestion(item: LabelSuggestion) {
- if (props.on_select) {
+ if (props.on_select !== undefined) {
props.on_select(item);
}
}
@@ -184,10 +192,12 @@ export const PredictionSuggestion: Component<{
set_error(undefined);
try {
const payload = {
+ suggestion_id: sg.suggestion_id,
block_start: sg.block_start,
block_end: sg.block_end,
label,
- ...(sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
+ overwrite: undefined,
+ allow_overlap: undefined,
};
await notification_accept(payload);
set_overwrite_pending(undefined);
@@ -232,11 +242,12 @@ export const PredictionSuggestion: Component<{
try {
const sg = s();
const payload = {
+ suggestion_id: sg?.suggestion_id,
block_start: pending.start,
block_end: pending.end,
label: pending.label,
overwrite: true as const,
- ...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
+ allow_overlap: undefined,
};
await notification_accept(payload);
set_overwrite_pending(undefined);
@@ -261,11 +272,12 @@ export const PredictionSuggestion: Component<{
try {
const sg = s();
const payload = {
+ suggestion_id: sg?.suggestion_id,
block_start: pending.start,
block_end: pending.end,
label: pending.label,
+ overwrite: undefined,
allow_overlap: true as const,
- ...(sg && sg.suggestion_id ? { suggestion_id: sg.suggestion_id } : {}),
};
await notification_accept(payload);
set_overwrite_pending(undefined);
@@ -673,7 +685,12 @@ export const PredictionSuggestion: Component<{
-
+
{(pending) => (
;
latest_tray_state: Accessor;
active_suggestion: Accessor;
- pending_suggestions?: Accessor;
- label_change_count?: Accessor;
+ pending_suggestions: Accessor | undefined;
+ label_change_count: Accessor | undefined;
ws_stats: Accessor;
train_state: Accessor;
- on_open_label_recorder?: () => void;
+ on_open_label_recorder: (() => void) | undefined;
}> = (props) => {
const [tab, set_tab] = createSignal("system");
diff --git a/src/taskclf/ui/frontend/src/components/status/StatusPanelTab.tsx b/src/taskclf/ui/frontend/src/components/status/StatusPanelTab.tsx
index de67658..df54949 100644
--- a/src/taskclf/ui/frontend/src/components/status/StatusPanelTab.tsx
+++ b/src/taskclf/ui/frontend/src/components/status/StatusPanelTab.tsx
@@ -7,8 +7,8 @@ const TABS: PanelTab[] = ["system", "history", "training"];
export const StatusPanelTab: Component<{
active: Accessor;
on_change: Setter;
- history_pending?: Accessor;
- on_history_pending_click?: () => void;
+ history_pending: Accessor | undefined;
+ on_history_pending_click: (() => void) | undefined;
}> = (props) => (
;
- pending_count?: Accessor
;
+ pending_count: Accessor | undefined;
}> = (props) => {
const sug = () => props.suggestion();
diff --git a/src/taskclf/ui/frontend/src/components/ui/StatusProgress.tsx b/src/taskclf/ui/frontend/src/components/ui/StatusProgress.tsx
index b570432..ec97d06 100644
--- a/src/taskclf/ui/frontend/src/components/ui/StatusProgress.tsx
+++ b/src/taskclf/ui/frontend/src/components/ui/StatusProgress.tsx
@@ -1,6 +1,8 @@
import type { Component } from "solid-js";
-export const StatusProgress: Component<{ pct: number; color?: string }> = (props) => (
+export const StatusProgress: Component<{ pct: number; color: string | undefined }> = (
+ props,
+) => (
= (props) => (
= (props) => {
const [open, set_open] = createSignal(props.default_open ?? false);
diff --git a/src/taskclf/ui/frontend/src/lib/api.ts b/src/taskclf/ui/frontend/src/lib/api.ts
index 295b43c..a09c610 100644
--- a/src/taskclf/ui/frontend/src/lib/api.ts
+++ b/src/taskclf/ui/frontend/src/lib/api.ts
@@ -59,7 +59,10 @@ export type ActivitySummary = FeatureSummary & {
message: string | undefined;
};
-async function api_json
(url: string, init?: RequestInit): Promise {
+async function api_json(
+ url: string,
+ init: RequestInit | undefined = undefined,
+): Promise {
const res = await fetch(url, init);
if (!res.ok) {
const text = await res.text().catch(() => "");
@@ -88,11 +91,11 @@ export async function label_create(body: {
start_ts: string;
end_ts: string;
label: string;
- user_id?: string;
- confidence?: number;
- extend_forward?: boolean;
- overwrite?: boolean;
- allow_overlap?: boolean;
+ user_id: string | undefined;
+ confidence: number | undefined;
+ extend_forward: boolean | undefined;
+ overwrite: boolean | undefined;
+ allow_overlap: boolean | undefined;
}): Promise {
return api_json(`${BASE}/labels`, {
method: "POST",
@@ -144,9 +147,9 @@ export async function label_update(body: {
start_ts: string;
end_ts: string;
label: string;
- new_start_ts?: string;
- new_end_ts?: string;
- extend_forward?: boolean;
+ new_start_ts: string | undefined;
+ new_end_ts: string | undefined;
+ extend_forward: boolean | undefined;
}): Promise {
return api_json(`${BASE}/labels`, {
method: "PUT",
@@ -171,12 +174,12 @@ export async function core_labels_list(): Promise {
}
export async function notification_accept(body: {
- suggestion_id?: string;
+ suggestion_id: string | undefined;
block_start: string;
block_end: string;
label: string;
- overwrite?: boolean;
- allow_overlap?: boolean;
+ overwrite: boolean | undefined;
+ allow_overlap: boolean | undefined;
}): Promise {
return api_json(`${BASE}/notification/accept`, {
method: "POST",
@@ -185,9 +188,9 @@ export async function notification_accept(body: {
});
}
-export async function notification_skip(body?: {
- suggestion_id?: string;
-}): Promise<{ status: string }> {
+export async function notification_skip(
+ body: { suggestion_id: string | undefined } = { suggestion_id: undefined },
+): Promise<{ status: string }> {
return api_json(`${BASE}/notification/skip`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -208,9 +211,9 @@ export async function user_config_get(): Promise {
}
export async function user_config_update(patch: {
- username?: string;
- suggestion_banner_ttl_seconds?: number;
- auto_save_suggestion_min_confidence?: number;
+ username: string | undefined;
+ suggestion_banner_ttl_seconds: number | undefined;
+ auto_save_suggestion_min_confidence: number | undefined;
}): Promise {
return api_json(`${BASE}/config/user`, {
method: "PUT",
@@ -260,9 +263,9 @@ export type DataCheck = {
export async function training_start(params: {
date_from: string;
date_to: string;
- num_boost_round?: number;
- class_weight?: "balanced" | "none";
- synthetic?: boolean;
+ num_boost_round: number | undefined;
+ class_weight: "balanced" | "none" | undefined;
+ synthetic: boolean | undefined;
}): Promise {
return api_json(`${BASE}/train/start`, {
method: "POST",
diff --git a/src/taskclf/ui/frontend/src/lib/host.test.ts b/src/taskclf/ui/frontend/src/lib/host.test.ts
index 96ed057..8af26b9 100644
--- a/src/taskclf/ui/frontend/src/lib/host.test.ts
+++ b/src/taskclf/ui/frontend/src/lib/host.test.ts
@@ -1,8 +1,8 @@
import { afterEach, describe, expect, it, vi } from "vitest";
function host_globals_clear() {
- delete (window as typeof window & { electronHost?: unknown }).electronHost;
- delete (window as typeof window & { pywebview?: unknown }).pywebview;
+ delete (window as typeof window & { electronHost: unknown | undefined }).electronHost;
+ delete (window as typeof window & { pywebview: unknown | undefined }).pywebview;
}
describe("host", () => {
@@ -15,7 +15,9 @@ describe("host", () => {
it("uses the Electron bridge when available", async () => {
const electron_invoke = vi.fn().mockResolvedValue(undefined);
(
- window as typeof window & { electronHost?: { invoke: typeof electron_invoke } }
+ window as typeof window & {
+ electronHost: { invoke: typeof electron_invoke } | undefined;
+ }
).electronHost = {
invoke: electron_invoke,
};
@@ -38,7 +40,9 @@ describe("host", () => {
const dashboard_toggle = vi.fn().mockResolvedValue(undefined);
(
window as typeof window & {
- pywebview?: { api?: { dashboard_toggle: typeof dashboard_toggle } };
+ pywebview:
+ | { api: { dashboard_toggle: typeof dashboard_toggle } | undefined }
+ | undefined;
}
).pywebview = {
api: {
@@ -70,11 +74,15 @@ describe("host", () => {
};
(
window as typeof window & {
- pywebview?: {
- api?: {
- show_transition_notification: typeof show_transition_notification;
- };
- };
+ pywebview:
+ | {
+ api:
+ | {
+ show_transition_notification: typeof show_transition_notification;
+ }
+ | undefined;
+ }
+ | undefined;
}
).pywebview = {
api: {
diff --git a/src/taskclf/ui/frontend/src/lib/host.ts b/src/taskclf/ui/frontend/src/lib/host.ts
index 130961a..bf8a9b0 100644
--- a/src/taskclf/ui/frontend/src/lib/host.ts
+++ b/src/taskclf/ui/frontend/src/lib/host.ts
@@ -39,26 +39,32 @@ export type Host = {
declare global {
interface Window {
- electronHost?: {
- invoke(command: HostCommand): Promise;
- };
- pywebview?: {
- api?: {
- label_grid_show(): Promise;
- label_grid_hide(): Promise;
- label_grid_toggle(): Promise;
- label_grid_cancel_hide(): Promise;
- show_transition_notification(prompt: PromptLabelEvent): Promise;
- window_hide(): Promise;
- dashboard_toggle(): Promise;
- state_panel_toggle(): Promise;
- state_panel_show(): Promise;
- state_panel_hide(): Promise;
- state_panel_cancel_hide(): Promise;
- frontend_debug_log(message: string): Promise;
- frontend_error_log(message: string): Promise;
- };
- };
+ electronHost:
+ | {
+ invoke(command: HostCommand): Promise;
+ }
+ | undefined;
+ pywebview:
+ | {
+ api:
+ | {
+ label_grid_show(): Promise;
+ label_grid_hide(): Promise;
+ label_grid_toggle(): Promise;
+ label_grid_cancel_hide(): Promise;
+ show_transition_notification(prompt: PromptLabelEvent): Promise;
+ window_hide(): Promise;
+ dashboard_toggle(): Promise;
+ state_panel_toggle(): Promise;
+ state_panel_show(): Promise;
+ state_panel_hide(): Promise;
+ state_panel_cancel_hide(): Promise;
+ frontend_debug_log(message: string): Promise;
+ frontend_error_log(message: string): Promise;
+ }
+ | undefined;
+ }
+ | undefined;
}
}
diff --git a/src/taskclf/ui/frontend/src/lib/labelTimeline.ts b/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
index e946613..9ef0ec9 100644
--- a/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
+++ b/src/taskclf/ui/frontend/src/lib/labelTimeline.ts
@@ -4,7 +4,7 @@ export type LabelEntry = {
label: string;
start_ts: string;
end_ts: string;
- extend_forward?: boolean;
+ extend_forward: boolean | undefined;
};
export type OpenEndedLabelLike = Pick<
@@ -30,7 +30,7 @@ export type LabelItem = {
label: string;
start_ts: string;
end_ts: string;
- open_ended?: boolean;
+ open_ended: boolean | undefined;
};
export type TimelineItem = GapItem | LabelItem;
diff --git a/src/taskclf/ui/frontend/src/lib/log.ts b/src/taskclf/ui/frontend/src/lib/log.ts
index 2f27e94..25c46d0 100644
--- a/src/taskclf/ui/frontend/src/lib/log.ts
+++ b/src/taskclf/ui/frontend/src/lib/log.ts
@@ -4,7 +4,7 @@ const FRONTEND_LOG_MAX_LEN = 1000;
function debug_enabled(): boolean {
const vite_meta = import.meta as ImportMeta & {
- env?: { DEV?: boolean };
+ env: { DEV: boolean | undefined } | undefined;
};
return vite_meta.env?.DEV === true;
}
diff --git a/src/taskclf/ui/frontend/src/lib/notifications.ts b/src/taskclf/ui/frontend/src/lib/notifications.ts
index 62caa1d..d53687e 100644
--- a/src/taskclf/ui/frontend/src/lib/notifications.ts
+++ b/src/taskclf/ui/frontend/src/lib/notifications.ts
@@ -5,8 +5,8 @@ import type { PromptLabelEvent } from "./ws";
// TypeScript's lib.dom.d.ts. Extend until upstream adds it.
// https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification#renotify
type NotificationOptionsExtended = NotificationOptions & {
- renotify?: boolean;
- requireInteraction?: boolean;
+ renotify: boolean | undefined;
+ requireInteraction: boolean | undefined;
};
let permission_granted = false;
diff --git a/src/taskclf/ui/frontend/src/lib/ws.ts b/src/taskclf/ui/frontend/src/lib/ws.ts
index 8cc7e11..0e7baca 100644
--- a/src/taskclf/ui/frontend/src/lib/ws.ts
+++ b/src/taskclf/ui/frontend/src/lib/ws.ts
@@ -27,13 +27,13 @@ export type Prediction = {
confidence: number;
ts: string;
mapped_label: string;
- current_app?: string;
- provenance?: "manual" | "model";
+ current_app: string | undefined;
+ provenance: "manual" | "model" | undefined;
};
export type LabelSuggestion = {
type: "suggest_label";
- suggestion_id?: string;
+ suggestion_id: string | undefined;
reason: string;
old_label: string;
suggested: string;
@@ -180,7 +180,7 @@ export type PromptLabelEvent = {
export type SuggestionClearedEvent = {
type: "suggestion_cleared";
reason: string;
- suggestion_id?: string;
+ suggestion_id: string | undefined;
};
export type SuggestionClearReason = "label_saved" | "skipped" | string;
@@ -236,7 +236,9 @@ export type TrainProgressEvent = {
export type TrainCompleteEvent = {
type: "train_complete";
job_id: string;
- metrics: { macro_f1?: number; weighted_f1?: number } | undefined;
+ metrics:
+ | { macro_f1: number | undefined; weighted_f1: number | undefined }
+ | undefined;
model_dir: string | undefined;
};
@@ -273,7 +275,9 @@ export type TrainState = {
progress_pct: number | undefined;
message: string | undefined;
error: string | undefined;
- metrics: { macro_f1?: number; weighted_f1?: number } | undefined;
+ metrics:
+ | { macro_f1: number | undefined; weighted_f1: number | undefined }
+ | undefined;
model_dir: string | undefined;
};
@@ -396,7 +400,7 @@ export function ws_store_new() {
function suggestion_active_set_in_state(
state: WebSocketStore,
- preferred_key?: string | undefined,
+ preferred_key: string | undefined = undefined,
) {
const current_key = state.active_suggestion
? label_suggestion_key(state.active_suggestion)
@@ -445,8 +449,8 @@ export function ws_store_new() {
}
function suggestion_queue_remove(
- reason?: SuggestionClearReason,
- suggestion_id?: string | undefined,
+ reason: SuggestionClearReason | undefined = undefined,
+ suggestion_id: string | undefined = undefined,
) {
setStore(
produce((state) => {
@@ -542,7 +546,10 @@ export function ws_store_new() {
}
}
- function ws_stats_bump(now: string, extra?: (s: WSStats) => void) {
+ function ws_stats_bump(
+ now: string,
+ extra: ((s: WSStats) => void) | undefined = undefined,
+ ) {
setStore(
"ws_stats",
produce((s) => {
@@ -801,8 +808,8 @@ export function ws_store_new() {
}
function suggestion_dismiss(
- reason?: SuggestionClearReason,
- suggestion?: LabelSuggestion,
+ reason: SuggestionClearReason | undefined = undefined,
+ suggestion: LabelSuggestion | undefined = undefined,
) {
suggestion_queue_remove(
reason,
From 7bab6ded5077855aa6c8fcd1cb93720b1a9197be Mon Sep 17 00:00:00 2001
From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com>
Date: Wed, 20 May 2026 17:40:38 +1000
Subject: [PATCH 06/11] chore: pnpm accept esbuild build scripts
---
src/taskclf/ui/frontend/package.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/taskclf/ui/frontend/package.json b/src/taskclf/ui/frontend/package.json
index 02ebc8c..c16cbd4 100644
--- a/src/taskclf/ui/frontend/package.json
+++ b/src/taskclf/ui/frontend/package.json
@@ -27,5 +27,10 @@
"vite": "^6.0.0",
"vite-plugin-solid": "^2.11.0",
"vitest": "^4.1.0"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "esbuild"
+ ]
}
}
From 32d94b380e9135b41945b1fc94b89c4349fc63b9 Mon Sep 17 00:00:00 2001
From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com>
Date: Wed, 20 May 2026 17:45:28 +1000
Subject: [PATCH 07/11] fix: pnpm accept esbuild build scripts package.json
deprecated
---
src/taskclf/ui/frontend/.npmrc | 1 +
src/taskclf/ui/frontend/package.json | 5 -----
2 files changed, 1 insertion(+), 5 deletions(-)
create mode 100644 src/taskclf/ui/frontend/.npmrc
diff --git a/src/taskclf/ui/frontend/.npmrc b/src/taskclf/ui/frontend/.npmrc
new file mode 100644
index 0000000..d13e841
--- /dev/null
+++ b/src/taskclf/ui/frontend/.npmrc
@@ -0,0 +1 @@
+only-built-dependencies[]=esbuild
diff --git a/src/taskclf/ui/frontend/package.json b/src/taskclf/ui/frontend/package.json
index c16cbd4..02ebc8c 100644
--- a/src/taskclf/ui/frontend/package.json
+++ b/src/taskclf/ui/frontend/package.json
@@ -27,10 +27,5 @@
"vite": "^6.0.0",
"vite-plugin-solid": "^2.11.0",
"vitest": "^4.1.0"
- },
- "pnpm": {
- "onlyBuiltDependencies": [
- "esbuild"
- ]
}
}
From 6e1e9846397d8261297524668735d8620d6bff48 Mon Sep 17 00:00:00 2001
From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com>
Date: Wed, 20 May 2026 17:50:53 +1000
Subject: [PATCH 08/11] ci: fix electron type errs
---
electron/port_conflict.ts | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/electron/port_conflict.ts b/electron/port_conflict.ts
index 1932663..0b18d56 100644
--- a/electron/port_conflict.ts
+++ b/electron/port_conflict.ts
@@ -111,13 +111,13 @@ function getListeningPidUnix(
platform: NodeJS.Platform,
spawnSyncFn: SpawnSyncFn,
): number | null {
- const lsof = spawnSyncFn("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
+ const lsof = spawnSyncFn("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], undefined);
const pidFromLsof = parseFirstPidFromLsofT(lsof.stdout);
if (pidFromLsof !== null) {
return pidFromLsof;
}
if (platform === "linux") {
- const ss = spawnSyncFn("ss", ["-lntp", `sport = :${port}`]);
+ const ss = spawnSyncFn("ss", ["-lntp", `sport = :${port}`], undefined);
if (ss.status === 0 && ss.stdout) {
return parsePidFromSsOutput(ss.stdout);
}
@@ -128,7 +128,7 @@ function getListeningPidUnix(
function getListeningPidWindows(port: number, spawnSyncFn: SpawnSyncFn): number | null {
const script =
`(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess)`;
- const ps = spawnSyncFn("powershell.exe", ["-NoProfile", "-Command", script]);
+ const ps = spawnSyncFn("powershell.exe", ["-NoProfile", "-Command", script], undefined);
if (ps.status !== 0 && ps.status !== null) {
return null;
}
@@ -148,14 +148,14 @@ function getProcessCommandLine(
if (platform === "win32") {
const script =
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
- const ps = spawnSyncFn("powershell.exe", ["-NoProfile", "-Command", script]);
+ const ps = spawnSyncFn("powershell.exe", ["-NoProfile", "-Command", script], undefined);
if (ps.status === 0 && ps.stdout.trim().length > 0) {
return ps.stdout.trim();
}
return "";
}
const field = platform === "linux" ? "args=" : "command=";
- const out = spawnSyncFn("ps", ["-p", String(pid), "-ww", "-o", field]);
+ const out = spawnSyncFn("ps", ["-p", String(pid), "-ww", "-o", field], undefined);
if (out.status === 0) {
return out.stdout.trim();
}
@@ -207,9 +207,9 @@ export async function killPidAndWaitForPortFree(
timeoutMs = DEFAULT_KILL_WAIT_MS,
): Promise {
if (platform === "win32") {
- spawnSyncFn("taskkill.exe", ["/PID", String(pid), "/T"]);
+ spawnSyncFn("taskkill.exe", ["/PID", String(pid), "/T"], undefined);
} else {
- spawnSyncFn("kill", ["-TERM", String(pid)]);
+ spawnSyncFn("kill", ["-TERM", String(pid)], undefined);
}
const deadline = Date.now() + timeoutMs;
@@ -221,9 +221,9 @@ export async function killPidAndWaitForPortFree(
}
if (platform === "win32") {
- spawnSyncFn("taskkill.exe", ["/PID", String(pid), "/T", "/F"]);
+ spawnSyncFn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], undefined);
} else {
- spawnSyncFn("kill", ["-KILL", String(pid)]);
+ spawnSyncFn("kill", ["-KILL", String(pid)], undefined);
}
const hardDeadline = Date.now() + 3000;
From faabea383e8e29f495e1d052ba80165afed66d74 Mon Sep 17 00:00:00 2001
From: fruitiecutiepie <104437268+fruitiecutiepie@users.noreply.github.com>
Date: Wed, 20 May 2026 18:34:14 +1000
Subject: [PATCH 09/11] ci: fix
---
src/taskclf/ui/frontend/src/App.tsx | 1 +
.../src/components/ActivitySummary.test.tsx | 27 ++-
.../src/components/ActivitySummary.tsx | 156 +++++++++---------
.../src/components/ErrorBanner.test.tsx | 2 +-
.../ui/frontend/src/components/LabelForm.tsx | 4 +
.../src/components/LabelHistory.test.tsx | 2 +-
.../src/components/LabelHistoryGapRow.tsx | 7 +-
.../src/components/LabelHistoryRow.tsx | 7 +-
.../src/components/LabelRecorder.test.tsx | 20 ++-
.../LabelRecorderActivitySummary.test.tsx | 12 +-
.../src/components/LabelRecorderWindow.tsx | 1 +
.../src/components/PredictionBadge.test.tsx | 11 ++
.../components/PredictionSuggestion.test.tsx | 54 ++++--
.../frontend/src/components/TrainingPanel.tsx | 50 +++++-
.../status/StatusActivityMonitor.tsx | 32 +++-
.../components/status/StatusActivityWatch.tsx | 20 ++-
.../src/components/status/StatusConfig.tsx | 18 +-
.../src/components/status/StatusModel.tsx | 26 ++-
.../components/status/StatusPrediction.tsx | 14 ++
.../components/status/StatusSuggestion.tsx | 14 ++
.../components/status/StatusTransitions.tsx | 19 ++-
.../src/components/status/StatusWebSocket.tsx | 14 ++
src/taskclf/ui/frontend/src/lib/ws.ts | 1 +
23 files changed, 397 insertions(+), 115 deletions(-)
diff --git a/src/taskclf/ui/frontend/src/App.tsx b/src/taskclf/ui/frontend/src/App.tsx
index 0f3942a..7f02243 100644
--- a/src/taskclf/ui/frontend/src/App.tsx
+++ b/src/taskclf/ui/frontend/src/App.tsx
@@ -350,6 +350,7 @@ const App: Component = () => {
}}
>
{
label_hide_cancel();
set_label_pinned(false);
diff --git a/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx b/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx
index 4ff169a..6d9c725 100644
--- a/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/ActivitySummary.test.tsx
@@ -64,7 +64,14 @@ describe("ActivitySummary", () => {
it("shows a no-data message for empty ranges", async () => {
vi.mocked(activity_summary_get).mockResolvedValueOnce(activity_summary_make());
- render(() => );
+ render(() => (
+
+ ));
expect(
await screen.findByText("No activity data for this window"),
@@ -85,7 +92,14 @@ describe("ActivitySummary", () => {
}),
);
- render(() => );
+ render(() => (
+
+ ));
expect(await screen.findByText("Activity source unavailable")).toBeInTheDocument();
expect(
@@ -96,7 +110,14 @@ describe("ActivitySummary", () => {
it("shows a generic fallback when the summary request fails", async () => {
vi.mocked(activity_summary_get).mockRejectedValueOnce(new Error("boom"));
- render(() => );
+ render(() => (
+
+ ));
await waitFor(() => {
expect(
diff --git a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
index ea0c723..ef81f7a 100644
--- a/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
+++ b/src/taskclf/ui/frontend/src/components/ActivitySummary.tsx
@@ -198,46 +198,20 @@ export const ActivitySummary: Component<{
- <>
-
-
-
- {(entry) => (
-
-
- {app_name_short(entry.app_id)}
-
- {entry.buckets}m
-
- )}
-
- }
- >
-
+
+
+
{(entry) => (
- {app_name_short(entry.app)}
+ {app_name_short(entry.app_id)}
- {entry.events}
+ {entry.buckets}m
)}
-
-
-
-
-
-
-
- {(v) => keys {v()}/m}
-
-
- {(v) => clicks {v()}/m}
-
-
- {(v) => scroll {v()}/m}
-
-
-
- {summary()?.total_buckets}m
- 1}>
- {" "}
- / {summary()?.session_count} sessions
-
-
-
-
-
- >
+
+ {(entry) => (
+
+
+ {app_name_short(entry.app)}
+
+ {entry.events}
+
+ )}
+
+
+
+
+
+
+
+
+ {(v) => keys {v()}/m}
+
+
+ {(v) => clicks {v()}/m}
+
+
+ {(v) => scroll {v()}/m}
+
+
+
+ {summary()?.total_buckets}m
+ 1}>
+ {" "}
+ / {summary()?.session_count} sessions
+
+
+
+
+
diff --git a/src/taskclf/ui/frontend/src/components/ErrorBanner.test.tsx b/src/taskclf/ui/frontend/src/components/ErrorBanner.test.tsx
index 7bc99a3..165878c 100644
--- a/src/taskclf/ui/frontend/src/components/ErrorBanner.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/ErrorBanner.test.tsx
@@ -14,7 +14,7 @@ describe("ErrorBanner", () => {
});
it("copies the current error text", async () => {
- render(() =>
);
+ render(() =>
);
fireEvent.click(screen.getByRole("button", { name: "Copy error" }));
diff --git a/src/taskclf/ui/frontend/src/components/LabelForm.tsx b/src/taskclf/ui/frontend/src/components/LabelForm.tsx
index 9f42064..24e7e9e 100644
--- a/src/taskclf/ui/frontend/src/components/LabelForm.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelForm.tsx
@@ -44,7 +44,11 @@ export const LabelForm: Component = () => {
start_ts: start_ts(),
end_ts: end_ts(),
label: label(),
+ user_id: undefined,
confidence: confidence(),
+ extend_forward: undefined,
+ overwrite: undefined,
+ allow_overlap: undefined,
});
set_status({
type: "success",
diff --git a/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx b/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx
index 082306a..ee62aa7 100644
--- a/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelHistory.test.tsx
@@ -89,7 +89,7 @@ describe("LabelHistory", () => {
const [visible, set_visible] = createSignal(false);
- render(() =>
);
+ render(() =>
);
vi.setSystemTime(new Date(2026, 3, 6, 10, 0, 0, 0));
const next_today = date_today_str();
diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx
index 7b7e671..56d5756 100644
--- a/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelHistoryGapRow.tsx
@@ -179,7 +179,12 @@ export const LabelHistoryGapRow: Component<{
/>
- selected_range()} />
+ selected_range()}
+ prediction={undefined}
+ show_empty={undefined}
+ />
diff --git a/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx b/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx
index 8b5577c..5895638 100644
--- a/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelHistoryRow.tsx
@@ -206,7 +206,12 @@ export const LabelHistoryRow: Component<{
- edited_range()} />
+ edited_range()}
+ prediction={undefined}
+ show_empty={undefined}
+ />
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx
index 0b420a5..82f0523 100644
--- a/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelRecorder.test.tsx
@@ -33,6 +33,16 @@ beforeEach(() => {
});
describe("LabelRecorder", () => {
+ const base_props = {
+ max_height: undefined,
+ prediction: undefined,
+ suggestion: undefined,
+ suggestions: undefined,
+ label_change_count: undefined,
+ on_suggestion_dismiss: undefined,
+ on_suggestion_select: undefined,
+ } as const;
+
it("shows a stop action for the current open-ended label and ends it at click time", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-05T10:00:00Z"));
@@ -80,7 +90,7 @@ describe("LabelRecorder", () => {
extend_forward: false,
});
- render(() => );
+ render(() => );
expect(await screen.findByText(/^Current:/)).toBeInTheDocument();
@@ -138,7 +148,7 @@ describe("LabelRecorder", () => {
extend_forward: true,
});
- render(() => );
+ render(() => );
expect(await screen.findByText(/^Current:/)).toBeInTheDocument();
expect(
@@ -168,7 +178,7 @@ describe("LabelRecorder", () => {
extend_forward: true,
});
- render(() => );
+ render(() => );
expect(await screen.findByText(/^Current:/)).toBeInTheDocument();
expect(
@@ -190,7 +200,7 @@ describe("LabelRecorder", () => {
},
]);
- render(() => );
+ render(() => );
await waitFor(() => {
expect(labels_list).toHaveBeenCalledWith(1);
@@ -231,7 +241,7 @@ describe("LabelRecorder", () => {
extend_forward: true,
});
- render(() => );
+ render(() => );
await waitFor(() => {
expect(screen.getByRole("button", { name: "gap 1h30m" })).toBeInTheDocument();
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
index 9515575..1d29531 100644
--- a/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelRecorderActivitySummary.test.tsx
@@ -20,6 +20,16 @@ vi.mock("../lib/api", () => ({
}));
describe("LabelRecorder activity summary", () => {
+ const base_props = {
+ max_height: undefined,
+ prediction: undefined,
+ suggestion: undefined,
+ suggestions: undefined,
+ label_change_count: undefined,
+ on_suggestion_dismiss: undefined,
+ on_suggestion_select: undefined,
+ } as const;
+
it("keeps manual labeling interactive when the provider is unavailable", async () => {
vi.mocked(core_labels_list).mockResolvedValue(["Build", "Write"]);
vi.mocked(current_label_get).mockResolvedValue(undefined);
@@ -74,7 +84,7 @@ describe("LabelRecorder activity summary", () => {
"Manual labeling still works, but activity summaries and automatic activity tracking are unavailable until this source is set up.",
});
- render(() => {}} />);
+ render(() => {}} />);
fireEvent.click(await screen.findByRole("button", { name: "1m" }));
diff --git a/src/taskclf/ui/frontend/src/components/LabelRecorderWindow.tsx b/src/taskclf/ui/frontend/src/components/LabelRecorderWindow.tsx
index 972c922..bf0e17b 100644
--- a/src/taskclf/ui/frontend/src/components/LabelRecorderWindow.tsx
+++ b/src/taskclf/ui/frontend/src/components/LabelRecorderWindow.tsx
@@ -39,6 +39,7 @@ export const LabelRecorderWindow: Component = () => {
>
{
label: undefined,
}),
active_suggestion: () => undefined,
+ label_pinned: () => false,
+ panel_pinned: () => false,
+ on_toggle_panel: undefined,
+ on_show_panel: undefined,
+ on_hide_panel: undefined,
+ on_toggle_label: undefined,
+ on_show_label: undefined,
+ on_hide_label: undefined,
};
it("falls back to live status when there is no latest prediction", () => {
@@ -95,6 +103,7 @@ describe("PredictionBadge", () => {
mapped_label: "Build",
confidence: 1,
ts: "2026-04-05T10:01:00Z",
+ current_app: undefined,
provenance: "manual",
})}
live_status={() => ({
@@ -119,6 +128,7 @@ describe("PredictionBadge", () => {
mapped_label: "Build",
confidence: 1,
ts: "2026-04-05T10:01:00Z",
+ current_app: undefined,
provenance: "manual",
})}
live_status={() => ({
@@ -147,6 +157,7 @@ describe("PredictionBadge", () => {
mapped_label: "Build",
confidence: 1,
ts: "2026-04-05T10:01:00Z",
+ current_app: undefined,
provenance: "manual",
})}
live_status={() => ({
diff --git a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
index b1b2b08..533abd4 100644
--- a/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
+++ b/src/taskclf/ui/frontend/src/components/PredictionSuggestion.test.tsx
@@ -82,6 +82,7 @@ beforeEach(() => {
function suggestion_make(overrides: Partial = {}): LabelSuggestion {
return {
type: "suggest_label",
+ suggestion_id: undefined,
reason: "App transition suggested a new label",
old_label: "ReadResearch",
suggested: "Write",
@@ -134,10 +135,19 @@ function overlap_error_make(): Error {
}
describe("PredictionSuggestion", () => {
+ const base_props = {
+ suggestions: undefined,
+ on_saved: undefined,
+ on_dismiss: undefined,
+ on_select: undefined,
+ } as const;
+
it("shows the applicable suggestion time range", () => {
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
expect(
screen.getByText(
@@ -165,6 +175,8 @@ describe("PredictionSuggestion", () => {
active}
suggestions={() => [active, later]}
+ on_saved={undefined}
+ on_dismiss={undefined}
on_select={on_select}
/>
));
@@ -180,7 +192,9 @@ describe("PredictionSuggestion", () => {
it("loads activity context for the suggestion block range", async () => {
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
await waitFor(() => {
expect(vi.mocked(activity_summary_get)).toHaveBeenCalledWith(
@@ -208,7 +222,9 @@ describe("PredictionSuggestion", () => {
);
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
expect(await screen.findByText("Activity source unavailable")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Use suggestion" }));
@@ -228,7 +244,9 @@ describe("PredictionSuggestion", () => {
block_end: new Date(2026, 3, 6, 0, 10, 0).toISOString(),
});
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
expect(
screen.getByText(
@@ -245,8 +263,10 @@ describe("PredictionSuggestion", () => {
render(() => (
suggestion}
+ suggestions={undefined}
on_saved={on_saved}
on_dismiss={on_dismiss}
+ on_select={undefined}
/>
));
@@ -266,7 +286,9 @@ describe("PredictionSuggestion", () => {
it("opens the correction panel from Change label", async () => {
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Change label" }));
@@ -282,7 +304,9 @@ describe("PredictionSuggestion", () => {
it("saves the selected correction label", async () => {
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Change label" }));
fireEvent.click(await screen.findByRole("button", { name: "Debug" }));
@@ -301,7 +325,9 @@ describe("PredictionSuggestion", () => {
vi.mocked(notification_accept).mockRejectedValueOnce(new Error("save failed"));
const suggestion = suggestion_make();
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Use suggestion" }));
@@ -345,8 +371,10 @@ describe("PredictionSuggestion", () => {
render(() => (
suggestion}
+ suggestions={undefined}
on_saved={on_saved}
on_dismiss={on_dismiss}
+ on_select={undefined}
/>
));
@@ -397,7 +425,9 @@ describe("PredictionSuggestion", () => {
extend_forward: false,
});
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Use suggestion" }));
@@ -432,7 +462,9 @@ describe("PredictionSuggestion", () => {
extend_forward: false,
});
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Change label" }));
fireEvent.click(await screen.findByRole("button", { name: "Debug" }));
@@ -469,7 +501,9 @@ describe("PredictionSuggestion", () => {
extend_forward: false,
});
- render(() => suggestion} />);
+ render(() => (
+ suggestion} />
+ ));
fireEvent.click(screen.getByRole("button", { name: "Change label" }));
fireEvent.click(await screen.findByRole("button", { name: "Debug" }));
diff --git a/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx b/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
index 52fc168..d68091d 100644
--- a/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
+++ b/src/taskclf/ui/frontend/src/components/TrainingPanel.tsx
@@ -24,6 +24,13 @@ import { StatusProgress } from "./ui/StatusProgress";
import { StatusRow } from "./ui/StatusRow";
import { StatusSection } from "./ui/StatusSection";
+const STATUS_ROW_DEFAULTS = {
+ color: undefined,
+ dim: undefined,
+ mono: undefined,
+ tooltip: undefined,
+} as const;
+
export const TrainingPanel: Component<{
train_state: Accessor;
}> = (props) => {
@@ -284,7 +291,12 @@ export const TrainingPanel: Component<{
return (
-
+