From b9d5182fb42262c7429872ecd74161f313906d54 Mon Sep 17 00:00:00 2001 From: Thomas Lant Date: Mon, 16 Feb 2026 18:06:21 +0000 Subject: [PATCH 1/2] Rework `faff start` UX to always prompt for all ASTRO fields Instead of treating intents as atomic pick-or-create units, the new flow always prompts for alias then all fields (Role, Impact, Subject, Mode), using historical session counts to weight suggestions per alias. After collection, we match-or-create an intent so the data layer is unchanged. Co-Authored-By: Claude Opus 4.6 --- src/faff_cli/start.py | 284 +++++++++++++++++++++++------------------- 1 file changed, 156 insertions(+), 128 deletions(-) diff --git a/src/faff_cli/start.py b/src/faff_cli/start.py index 25c5657..4fdd1c9 100644 --- a/src/faff_cli/start.py +++ b/src/faff_cli/start.py @@ -1,5 +1,6 @@ import typer from typing import Sequence, List +from collections import defaultdict from titlecase import titlecase @@ -41,120 +42,93 @@ def nicer_tracker(strings: Sequence[str], ws: Workspace) -> list[str | FuzzyItem for s in strings ] -def print_sentence(role=None, action=None, objective=None, subject=None): +def print_sentence(role=None, impact=None, subject=None, mode=None): """Print the intent sentence with filled/unfilled parts.""" role_str = f"[bold cyan]{prettify_path_label(role)}[/bold cyan]" if role else "[dim]________[/dim]" - action_str = f"[bold cyan]{prettify_path_label(action)}[/bold cyan]" if action else "[dim]________[/dim]" - objective_str = f"[bold cyan]{prettify_path_label(objective)}[/bold cyan]" if objective else "[dim]________[/dim]" + mode_str = f"[bold cyan]{prettify_path_label(mode)}[/bold cyan]" if mode else "[dim]________[/dim]" + impact_str = f"[bold cyan]{prettify_path_label(impact)}[/bold cyan]" if impact else "[dim]________[/dim]" subject_str = f"[bold cyan]{prettify_path_label(subject)}[/bold cyan]" if subject else "[dim]________[/dim]" from rich.console import Console console = Console() - sentence = f" [dim]→[/dim] As a {role_str}, I am {action_str} to achieve {objective_str}, focused on {subject_str}." + sentence = f" [dim]\u2192[/dim] As a {role_str}, I am {mode_str} to achieve {impact_str}, focused on {subject_str}." console.print(f"\n{sentence}\n") -def input_new_intent(alias: str, ws: Workspace) -> Intent: - """ - Prompt the user for details to create a new intent. - """ - from rich.console import Console - console = Console() - date = ws.today() - console.print() - console.print("[bold green]✓[/bold green] Great! Now, let's capture the details.", style="bold") - console.print("[dim]Complete this sentence to describe what you're doing:[/dim]") - print_sentence() - - console.print("[bold]What role are you performing here?[/bold]") - console.print("[dim]e.g. Line Manager, Pre-Sales Engineer, Parent[/dim]") - role, _ = fuzzy_select( - "Role:", - nicer([x for x in ws.plans.get_roles(date)]), - escapable=True - ) - print_sentence(role=role.value if role else None) - - console.print("[bold]What action are you doing?[/bold]") - console.print("[dim]e.g. Planning, Attending a Scheduled Meeting[/dim]") - action, _ = fuzzy_select( - "Action:", - nicer([x for x in ws.plans.get_actions(date)]), - escapable=True - ) - print_sentence(role=role.value if role else None, action=action.value if action else None) - - console.print("[bold]What outcome are you aiming for?[/bold]") - console.print("[dim]e.g. Career Development, New Revenue New Business[/dim]") - objective, _ = fuzzy_select( - "Outcome:", - nicer([x for x in ws.plans.get_objectives(date)]), - escapable=True - ) - print_sentence(role=role.value if role else None, action=action.value if action else None, objective=objective.value if objective else None) - - console.print("[bold]Who or what are you focused on here?[/bold]") - console.print("[dim]e.g. John Smith, ACME Corporation, Sales Department[/dim]") - subject, _ = fuzzy_select( - "Focus:", - nicer([x for x in ws.plans.get_subjects(date)]), - escapable=True - ) - print_sentence(role=role.value if role else None, action=action.value if action else None, objective=objective.value if objective else None, subject=subject.value if subject else None) +def get_alias_choices(intents, session_counts_by_id): + """Deduplicate intents by alias and sum session counts per alias.""" + alias_totals = defaultdict(int) + for intent in intents: + alias_totals[intent.alias] += session_counts_by_id.get(intent.intent_id, 0) - trackers: List[str] = [] - all_trackers = list(ws.plans.get_trackers(date)) + sorted_aliases = sorted(alias_totals.items(), key=lambda x: x[1], reverse=True) - # Only show tracker selection if there are trackers available - if all_trackers: - ingesting_trackers = True + choices = [] + for alias, count in sorted_aliases: + decoration = f"({count})" if count > 0 else None + choices.append(FuzzyItem(name=alias, value=alias, decoration=decoration)) - console.print("[bold]Add remote trackers?[/bold]") - console.print("[dim]Select trackers or press Enter on empty line to finish.[/dim]") + return choices - while ingesting_trackers: - remaining = [x for x in all_trackers if x not in trackers] - if not remaining: - break - # Add a "Done" option at the top - choices = [FuzzyItem(name="[Done]", value=None, decoration="")] + nicer_tracker(remaining, ws) +def get_weighted_field_choices(field_name, alias, all_intents, all_values, session_counts_by_id): + """Build weighted FuzzyItem list for a field, with alias-correlated values first.""" + matching = [i for i in all_intents if i.alias == alias] - tracker_id, _ = fuzzy_select( - prompt="Tracker:", - choices=choices, - escapable=True, - create_new=False - ) - if tracker_id and tracker_id.value: - trackers.append(tracker_id.value) - else: - ingesting_trackers = False + field_freq = defaultdict(int) + for intent in matching: + val = getattr(intent, field_name, None) + if val: + field_freq[val] += session_counts_by_id.get(intent.intent_id, 0) - local_plan = ws.plans.get_local_plan_or_create(date) + correlated = sorted(field_freq.items(), key=lambda x: x[1], reverse=True) + correlated_set = {v for v, _ in correlated} + + others = [v for v in all_values if v not in correlated_set] + + choices = [] + for val, count in correlated: + decoration = f"({count})" if count > 0 else None + choices.append(FuzzyItem(name=prettify_path_label(val), value=val, decoration=decoration)) + + for val in others: + choices.append(FuzzyItem(name=prettify_path_label(val), value=val, decoration=None)) + return choices + + +def match_or_create_intent(ws, alias, role, objective, action, subject, trackers): + """Find an existing intent matching all fields, or create a new one.""" + date = ws.today() + existing_intents = ws.plans.get_intents(date) + + for intent in existing_intents: + if (intent.alias == alias and + intent.role == role and + intent.objective == objective and + intent.action == action and + intent.subject == subject and + sorted(intent.trackers or []) == sorted(trackers or [])): + return intent + + # Create new intent + local_plan = ws.plans.get_local_plan_or_create(date) new_intent = Intent( alias=alias, - role=role.value if role else None, - objective=objective.value if objective else None, - action=action.value if action else None, - subject=subject.value if subject else None, + role=role, + objective=objective, + action=action, + subject=subject, trackers=trackers ) - new_plan = local_plan.add_intent(new_intent) ws.plans.write_plan(new_plan) - # Get the intent back from the plan - it now has a generated ID + # Get the intent back with generated ID intent_with_id = [i for i in new_plan.intents if i.alias == alias][-1] + return intent_with_id - # Show final success message with complete intent - console.print() - console.print("[bold green]✓[/bold green] Intent created!", style="bold") - print_sentence(role=role.value if role else None, action=action.value if action else None, objective=objective.value if objective else None, subject=subject.value if subject else None) - - return intent_with_id @app.callback(invoke_without_command=True) def start( @@ -169,7 +143,6 @@ def start( # Determine start time if continue_from_last: - # Get the last session's end time log = ws.logs.get_log(date) if not log.timeline: typer.echo("No previous session found to continue from", err=True) @@ -182,67 +155,122 @@ def start( start_time = last_session.end elif since: - # Parse the provided time (restricted to today) start_time = ws.parse_natural_datetime(since) else: - # Capture current time NOW, before any prompts start_time = ws.now() + # Compute session counts once upfront + session_counts = defaultdict(int) + for log in ws.logs.list_logs(): + for session in log.timeline: + session_counts[session.intent.intent_id] += 1 + existing_intents = ws.plans.get_intents(date) from rich.console import Console console = Console() + + # Step 1: Alias selection console.print() - console.print("[bold]What are you doing?[/bold]") - console.print("[dim]What would you call this activity if you were naming an entry in your calendar?[/dim]") + console.print("[bold]I am doing/making/attending:[/bold]") + console.print("[dim]Name this activity as you'd name a calendar entry[/dim]") console.print() - chosen_intent, _ = fuzzy_select( + alias_choices = get_alias_choices(existing_intents, session_counts) + chosen_alias, _ = fuzzy_select( prompt="Activity:", - choices=intents_to_choices(existing_intents), + choices=alias_choices, escapable=False, slugify_new=False, - ) + ) - # If the intent is new, we'll want to prompt for details. - if not chosen_intent: + if not chosen_alias: typer.echo("aborting") return - if chosen_intent.is_new: - intent = input_new_intent(chosen_intent.value, ws) - else: - intent = chosen_intent.value + + alias = chosen_alias.value + + # Step 2: Role + console.print("[bold]What role are you performing?[/bold]") + console.print("[dim]e.g. Line Manager, Pre-Sales Engineer, Parent[/dim]") + role_choices = get_weighted_field_choices( + "role", alias, existing_intents, + list(ws.plans.get_roles(date)), session_counts + ) + role, _ = fuzzy_select("Role:", role_choices, escapable=True) + role_val = role.value if role else None + print_sentence(role=role_val) + + # Step 3: Impact (maps to objective in data model) + console.print("[bold]What outcome are you aiming for?[/bold]") + console.print("[dim]e.g. Career Development, New Revenue[/dim]") + impact_choices = get_weighted_field_choices( + "objective", alias, existing_intents, + list(ws.plans.get_objectives(date)), session_counts + ) + impact, _ = fuzzy_select("Impact:", impact_choices, escapable=True) + impact_val = impact.value if impact else None + print_sentence(role=role_val, impact=impact_val) + + # Step 4: Subject + console.print("[bold]Who or what are you focused on?[/bold]") + console.print("[dim]e.g. John Smith, ACME Corporation[/dim]") + subject_choices = get_weighted_field_choices( + "subject", alias, existing_intents, + list(ws.plans.get_subjects(date)), session_counts + ) + subject, _ = fuzzy_select("Subject:", subject_choices, escapable=True) + subject_val = subject.value if subject else None + print_sentence(role=role_val, impact=impact_val, subject=subject_val) + + # Step 5: Mode (maps to action in data model) + console.print("[bold]What are you doing?[/bold]") + console.print("[dim]e.g. Planning, Attending a Scheduled Meeting[/dim]") + mode_choices = get_weighted_field_choices( + "action", alias, existing_intents, + list(ws.plans.get_actions(date)), session_counts + ) + mode, _ = fuzzy_select("Mode:", mode_choices, escapable=True) + mode_val = mode.value if mode else None + print_sentence(role=role_val, impact=impact_val, subject=subject_val, mode=mode_val) + + # Step 6: Trackers + trackers: List[str] = [] + all_trackers = list(ws.plans.get_trackers(date)) + + if all_trackers: + console.print("[bold]Add remote trackers?[/bold]") + console.print("[dim]Select trackers or press Enter on empty line to finish.[/dim]") + + ingesting_trackers = True + while ingesting_trackers: + remaining = [x for x in all_trackers if x not in trackers] + if not remaining: + break + + choices = [FuzzyItem(name="[Done]", value=None, decoration="")] + nicer_tracker(remaining, ws) + + tracker_id, _ = fuzzy_select( + prompt="Tracker:", + choices=choices, + escapable=True, + create_new=False + ) + if tracker_id and tracker_id.value: + trackers.append(tracker_id.value) + else: + ingesting_trackers = False + + # Step 7: Match or create intent + intent = match_or_create_intent( + ws, alias, role_val, impact_val, mode_val, subject_val, trackers + ) + + # Step 8: Note + start session note = input("? Note for this session (optional): ") - # Rust core handles validation (future time, overlaps) and auto-stops active session ws.logs.start_intent(intent, start_time, note if note else None) typer.echo(f"Started '{intent.alias}' at {start_time.strftime('%H:%M')}") except Exception as e: typer.echo(f"Error starting session: {e}", err=True) raise typer.Exit(1) - -def intents_to_choices(intents): - """ - Convert intents to fuzzy select choices. - If multiple intents share the same alias, disambiguate by adding the intent_id. - """ - # Count alias occurrences to detect duplicates - alias_counts = {} - for intent in intents: - alias_counts[intent.alias] = alias_counts.get(intent.alias, 0) + 1 - - choices = [] - for intent in intents: - # If this alias appears more than once, disambiguate with intent_id - if alias_counts[intent.alias] > 1: - display_name = f"{intent.alias} ({intent.intent_id})" - else: - display_name = intent.alias - - choices.append({ - "name": display_name, - "value": intent, - "decoration": None - }) - - return choices From 52cea2719836d1e62b5e681e2833e492975d4c31 Mon Sep 17 00:00:00 2001 From: Thomas Lant Date: Sat, 28 Feb 2026 23:23:42 +0000 Subject: [PATCH 2/2] Improve faff start UX: conditional field weighting, tracker defaults, and cleanup - Each field now conditions on all previously chosen fields (not just alias), so suggestions narrow progressively as the user makes choices - Tracker choices weighted by full context; defaults to most correlated tracker if one exists, [No trackers] otherwise - Raw path/ID always shown in decoration alongside session count - Blank labels filtered from all choice lists - Tracker done-item label changes from [No trackers] to [Done] once one is picked - Homogenised spacing across all prompt steps - Extracted _filter_intents helper (was duplicated across two functions) - Extracted _prompt_for_intent to separate UI from command orchestration - Removed dead code (print_sentence, nicer_tracker); restored nicer for intent.py - Dropped typing.List/Sequence imports; use built-in generics Co-Authored-By: Claude Sonnet 4.6 --- src/faff_cli/start.py | 293 +++++++++++++++++++++++------------------- 1 file changed, 158 insertions(+), 135 deletions(-) diff --git a/src/faff_cli/start.py b/src/faff_cli/start.py index 4fdd1c9..7a170ee 100644 --- a/src/faff_cli/start.py +++ b/src/faff_cli/start.py @@ -1,14 +1,11 @@ import typer -from typing import Sequence, List from collections import defaultdict from titlecase import titlecase - from faff_cli.ui import FuzzyItem, fuzzy_select from faff_core import Workspace - from faff_core.models import Intent app = typer.Typer(help="Start a new task or activity.") @@ -29,31 +26,23 @@ def prettify_path_label(path: str) -> str: return f"{name} ({namespace}:{path})" if context else name -def nicer(strings: Sequence[str]) -> list[str | FuzzyItem]: +def nicer(strings) -> list[FuzzyItem]: return [ FuzzyItem(name=prettify_path_label(s), value=s, decoration=s) for s in strings ] -def nicer_tracker(strings: Sequence[str], ws: Workspace) -> list[str | FuzzyItem]: - trackers = ws.plans.get_trackers(ws.today()) - return [ - FuzzyItem(name=trackers.get(s, ''), value=s, decoration=s) - for s in strings - ] - -def print_sentence(role=None, impact=None, subject=None, mode=None): - """Print the intent sentence with filled/unfilled parts.""" - role_str = f"[bold cyan]{prettify_path_label(role)}[/bold cyan]" if role else "[dim]________[/dim]" - mode_str = f"[bold cyan]{prettify_path_label(mode)}[/bold cyan]" if mode else "[dim]________[/dim]" - impact_str = f"[bold cyan]{prettify_path_label(impact)}[/bold cyan]" if impact else "[dim]________[/dim]" - subject_str = f"[bold cyan]{prettify_path_label(subject)}[/bold cyan]" if subject else "[dim]________[/dim]" - from rich.console import Console - console = Console() - - sentence = f" [dim]\u2192[/dim] As a {role_str}, I am {mode_str} to achieve {impact_str}, focused on {subject_str}." - console.print(f"\n{sentence}\n") +def _filter_intents(all_intents, alias, context=None): + """Filter intents by alias, then by context fields, falling back to alias-only if nothing matches.""" + alias_matching = [i for i in all_intents if i.alias == alias] + if not context: + return alias_matching + matching = [ + i for i in alias_matching + if all(getattr(i, k, None) == v for k, v in context.items() if v is not None) + ] + return matching if matching else alias_matching def get_alias_choices(intents, session_counts_by_id): @@ -72,9 +61,9 @@ def get_alias_choices(intents, session_counts_by_id): return choices -def get_weighted_field_choices(field_name, alias, all_intents, all_values, session_counts_by_id): - """Build weighted FuzzyItem list for a field, with alias-correlated values first.""" - matching = [i for i in all_intents if i.alias == alias] +def get_weighted_field_choices(field_name, alias, all_intents, all_values, session_counts_by_id, context=None): + """Build weighted FuzzyItem list for a field, conditioned on alias and any previously chosen fields.""" + matching = _filter_intents(all_intents, alias, context) field_freq = defaultdict(int) for intent in matching: @@ -84,22 +73,62 @@ def get_weighted_field_choices(field_name, alias, all_intents, all_values, sessi correlated = sorted(field_freq.items(), key=lambda x: x[1], reverse=True) correlated_set = {v for v, _ in correlated} - others = [v for v in all_values if v not in correlated_set] choices = [] for val, count in correlated: - decoration = f"({count})" if count > 0 else None - choices.append(FuzzyItem(name=prettify_path_label(val), value=val, decoration=decoration)) + name = prettify_path_label(val) + if not name: + continue + decoration = f"{val} ({count})" if count > 0 else val + choices.append(FuzzyItem(name=name, value=val, decoration=decoration)) for val in others: - choices.append(FuzzyItem(name=prettify_path_label(val), value=val, decoration=None)) + name = prettify_path_label(val) + if not name: + continue + choices.append(FuzzyItem(name=name, value=val, decoration=val)) return choices +def get_weighted_tracker_choices(alias, all_intents, all_tracker_ids, tracker_names, session_counts_by_id, context=None, chosen_trackers=None): + """Build weighted tracker choices conditioned on alias and previously chosen fields. + + Returns (choices, has_correlated) where has_correlated indicates whether any + trackers were historically associated with this context. + """ + matching = _filter_intents(all_intents, alias, context) + + tracker_freq = defaultdict(int) + for intent in matching: + for tracker in (intent.trackers or []): + tracker_freq[tracker] += session_counts_by_id.get(intent.intent_id, 0) + + chosen_set = set(chosen_trackers or []) + remaining = [t for t in all_tracker_ids if t not in chosen_set] + + correlated = sorted( + [(t, tracker_freq[t]) for t in remaining if t in tracker_freq], + key=lambda x: x[1], reverse=True + ) + correlated_set = {t for t, _ in correlated} + others = [t for t in remaining if t not in correlated_set] + + choices = [] + for tracker_id, count in correlated: + name = tracker_names.get(tracker_id, tracker_id) + decoration = f"{tracker_id} ({count})" if count > 0 else tracker_id + choices.append(FuzzyItem(name=name, value=tracker_id, decoration=decoration)) + for tracker_id in others: + name = tracker_names.get(tracker_id, tracker_id) + choices.append(FuzzyItem(name=name, value=tracker_id, decoration=tracker_id)) + + return choices, bool(correlated) + + def match_or_create_intent(ws, alias, role, objective, action, subject, trackers): - """Find an existing intent matching all fields, or create a new one.""" + """Find an existing intent matching all fields, or create and persist a new one.""" date = ws.today() existing_intents = ws.plans.get_intents(date) @@ -112,7 +141,6 @@ def match_or_create_intent(ws, alias, role, objective, action, subject, trackers sorted(intent.trackers or []) == sorted(trackers or [])): return intent - # Create new intent local_plan = ws.plans.get_local_plan_or_create(date) new_intent = Intent( alias=alias, @@ -125,9 +153,103 @@ def match_or_create_intent(ws, alias, role, objective, action, subject, trackers new_plan = local_plan.add_intent(new_intent) ws.plans.write_plan(new_plan) - # Get the intent back with generated ID - intent_with_id = [i for i in new_plan.intents if i.alias == alias][-1] - return intent_with_id + # Get the intent back with its generated ID + return [i for i in new_plan.intents if i.alias == alias][-1] + + +def _prompt_for_intent(ws, existing_intents, session_counts): + """Interactively prompt for all intent fields. Returns the matched/created Intent, or None if aborted.""" + from rich.console import Console + console = Console() + date = ws.today() + + # Step 1: Alias + console.print() + console.print("[bold]I am doing/making/attending:[/bold]") + console.print("[dim]e.g. 1:1 with Yan, Monthly Strategy Meeting[/dim]") + chosen_alias, _ = fuzzy_select( + prompt="Title:", + choices=get_alias_choices(existing_intents, session_counts), + escapable=False, + slugify_new=False, + ) + if not chosen_alias: + return None + alias = chosen_alias.value + + # Step 2: Role + console.print() + console.print("[bold]I am performing the role of:[/bold]") + console.print("[dim]e.g. Line Manager, Pre-Sales Engineer, Parent[/dim]") + role, _ = fuzzy_select("Role:", get_weighted_field_choices( + "role", alias, existing_intents, + list(ws.plans.get_roles(date)), session_counts, + ), escapable=True) + role_val = role.value if role else None + + # Step 3: Impact (objective in data model) + console.print() + console.print("[bold]I hope the impact is:[/bold]") + console.print("[dim]e.g. Career Development, New Revenue[/dim]") + impact, _ = fuzzy_select("Impact:", get_weighted_field_choices( + "objective", alias, existing_intents, + list(ws.plans.get_objectives(date)), session_counts, + context={"role": role_val}, + ), escapable=True) + impact_val = impact.value if impact else None + + # Step 4: Subject + console.print() + console.print("[bold]I am mostly focused on:[/bold]") + console.print("[dim]e.g. John Smith, ACME Corporation[/dim]") + subject, _ = fuzzy_select("Subject:", get_weighted_field_choices( + "subject", alias, existing_intents, + list(ws.plans.get_subjects(date)), session_counts, + context={"role": role_val, "objective": impact_val}, + ), escapable=True) + subject_val = subject.value if subject else None + + # Step 5: Mode (action in data model) + console.print() + console.print("[bold]My primary mode is:[/bold]") + console.print("[dim]e.g. Planning, Meeting, Testing[/dim]") + mode, _ = fuzzy_select("Mode:", get_weighted_field_choices( + "action", alias, existing_intents, + list(ws.plans.get_actions(date)), session_counts, + context={"role": role_val, "objective": impact_val, "subject": subject_val}, + ), escapable=True) + mode_val = mode.value if mode else None + + # Step 6: Trackers + trackers: list[str] = [] + tracker_names = ws.plans.get_trackers(date) + all_tracker_ids = list(tracker_names.keys()) + + if all_tracker_ids: + console.print() + console.print("[bold]Time spent in this session should be logged to:[/bold]") + console.print("[dim]e.g. PROJ-123, Billable: ACME[/dim]") + + full_context = {"role": role_val, "objective": impact_val, "subject": subject_val, "action": mode_val} + while True: + if all(t in trackers for t in all_tracker_ids): + break + + weighted, has_correlated = get_weighted_tracker_choices( + alias, existing_intents, all_tracker_ids, tracker_names, + session_counts, context=full_context, chosen_trackers=trackers, + ) + done_label = "[No trackers]" if not trackers else "[Done]" + done_item = FuzzyItem(name=done_label, value=None, decoration=None) + choices = weighted + [done_item] if has_correlated else [done_item] + weighted + + picked, _ = fuzzy_select(prompt="Tracker:", choices=choices, escapable=True, create_new=False) + if picked and picked.value: + trackers.append(picked.value) + else: + break + + return match_or_create_intent(ws, alias, role_val, impact_val, mode_val, subject_val, trackers) @app.callback(invoke_without_command=True) @@ -141,25 +263,21 @@ def start( ws: Workspace = ctx.obj date = ws.today() - # Determine start time if continue_from_last: log = ws.logs.get_log(date) if not log.timeline: typer.echo("No previous session found to continue from", err=True) raise typer.Exit(1) - last_session = log.timeline[-1] if last_session.end is None: typer.echo("Previous session is still active", err=True) raise typer.Exit(1) - start_time = last_session.end elif since: start_time = ws.parse_natural_datetime(since) else: start_time = ws.now() - # Compute session counts once upfront session_counts = defaultdict(int) for log in ws.logs.list_logs(): for session in log.timeline: @@ -167,106 +285,11 @@ def start( existing_intents = ws.plans.get_intents(date) - from rich.console import Console - console = Console() - - # Step 1: Alias selection - console.print() - console.print("[bold]I am doing/making/attending:[/bold]") - console.print("[dim]Name this activity as you'd name a calendar entry[/dim]") - console.print() - - alias_choices = get_alias_choices(existing_intents, session_counts) - chosen_alias, _ = fuzzy_select( - prompt="Activity:", - choices=alias_choices, - escapable=False, - slugify_new=False, - ) - - if not chosen_alias: + intent = _prompt_for_intent(ws, existing_intents, session_counts) + if not intent: typer.echo("aborting") return - alias = chosen_alias.value - - # Step 2: Role - console.print("[bold]What role are you performing?[/bold]") - console.print("[dim]e.g. Line Manager, Pre-Sales Engineer, Parent[/dim]") - role_choices = get_weighted_field_choices( - "role", alias, existing_intents, - list(ws.plans.get_roles(date)), session_counts - ) - role, _ = fuzzy_select("Role:", role_choices, escapable=True) - role_val = role.value if role else None - print_sentence(role=role_val) - - # Step 3: Impact (maps to objective in data model) - console.print("[bold]What outcome are you aiming for?[/bold]") - console.print("[dim]e.g. Career Development, New Revenue[/dim]") - impact_choices = get_weighted_field_choices( - "objective", alias, existing_intents, - list(ws.plans.get_objectives(date)), session_counts - ) - impact, _ = fuzzy_select("Impact:", impact_choices, escapable=True) - impact_val = impact.value if impact else None - print_sentence(role=role_val, impact=impact_val) - - # Step 4: Subject - console.print("[bold]Who or what are you focused on?[/bold]") - console.print("[dim]e.g. John Smith, ACME Corporation[/dim]") - subject_choices = get_weighted_field_choices( - "subject", alias, existing_intents, - list(ws.plans.get_subjects(date)), session_counts - ) - subject, _ = fuzzy_select("Subject:", subject_choices, escapable=True) - subject_val = subject.value if subject else None - print_sentence(role=role_val, impact=impact_val, subject=subject_val) - - # Step 5: Mode (maps to action in data model) - console.print("[bold]What are you doing?[/bold]") - console.print("[dim]e.g. Planning, Attending a Scheduled Meeting[/dim]") - mode_choices = get_weighted_field_choices( - "action", alias, existing_intents, - list(ws.plans.get_actions(date)), session_counts - ) - mode, _ = fuzzy_select("Mode:", mode_choices, escapable=True) - mode_val = mode.value if mode else None - print_sentence(role=role_val, impact=impact_val, subject=subject_val, mode=mode_val) - - # Step 6: Trackers - trackers: List[str] = [] - all_trackers = list(ws.plans.get_trackers(date)) - - if all_trackers: - console.print("[bold]Add remote trackers?[/bold]") - console.print("[dim]Select trackers or press Enter on empty line to finish.[/dim]") - - ingesting_trackers = True - while ingesting_trackers: - remaining = [x for x in all_trackers if x not in trackers] - if not remaining: - break - - choices = [FuzzyItem(name="[Done]", value=None, decoration="")] + nicer_tracker(remaining, ws) - - tracker_id, _ = fuzzy_select( - prompt="Tracker:", - choices=choices, - escapable=True, - create_new=False - ) - if tracker_id and tracker_id.value: - trackers.append(tracker_id.value) - else: - ingesting_trackers = False - - # Step 7: Match or create intent - intent = match_or_create_intent( - ws, alias, role_val, impact_val, mode_val, subject_val, trackers - ) - - # Step 8: Note + start session note = input("? Note for this session (optional): ") ws.logs.start_intent(intent, start_time, note if note else None)