Skip to content

Commit cd13d9b

Browse files
Name a session after the first substance logged into it
Starting a session from the phone is one tap with nowhere to type a title, so those entries landed in the journal as "Untitled" and stayed there. `name_after_first_dose` (db.rs, called from `log_dose`) fills a blank title with the substance name of the session's first dose. Only the first dose names it: after that the session has a name, and a title cleared back to blank was cleared on purpose. A title the user typed is never overwritten, and renaming still wins. Living at the db layer means it holds for the desktop, the phone, and the Companion's log_dose tool alike. The desktop's "+ Session" form stamped the literal string "Untitled experience" into the title column when the field was blank, which would have made the auto-name unreachable there; it now stores blank and leans on the display fallback the journal already had. The phone's start-a-session pane gains an optional title field — offered, never demanded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 17cfc26 commit cd13d9b

6 files changed

Lines changed: 137 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ after publishing — see RELEASING.md).
1111

1212
The heading must be exactly `## vX.Y.Z`, matching the tag. Newest on top.
1313

14+
## v0.11.3
15+
16+
- **Sessions name themselves.** Starting a session from your phone gave you
17+
nowhere to type a title, so it landed in the journal as "Untitled" and usually
18+
stayed that way. Now an untitled session takes the name of the first substance
19+
you log into it — so a session you started one-handed in the dark shows up as
20+
"ketamine" rather than "Untitled". Only the first dose names it, a title you
21+
typed yourself is never overwritten, and you can still rename anything at any
22+
time by tapping its title on the phone or editing it on the desktop.
23+
- **A title field when you start a session on the phone.** Optional — leave it
24+
blank and the naming above takes over.
25+
1426
## v0.11.2
1527

1628
- **Phone access no longer takes over a port another app is using.** If something

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ from your phone.
2929
happened" on a new session and it's saved as a finished trip, with doses
3030
defaulting to when it occurred rather than now. Every timestamp in a session
3131
also shows the time since your first dose (`14:02 (t+1:23)`), so the timeline
32-
reads against the clock that matters.
32+
reads against the clock that matters. A session you never got round to titling
33+
takes the name of the first substance you log into it, so nothing sits in the
34+
journal as "Untitled" — rename it whenever you like.
3335
- **Plain notes** — not everything is a session. Write ordinary journal entries
3436
(a title, your words, a date) alongside them.
3537
- **Combination warnings** — every dose is checked against the others for
@@ -72,8 +74,10 @@ from your phone.
7274
shows a green **"Paired successfully"** light the moment the phone first uses
7375
the code, so you're not left guessing whether the scan took. Companion
7476
replies run as a background job on the desktop, so a slow local model — or a
75-
locked phone screen — no longer drops the answer. Rename a session or entry
76-
right from the phone by tapping its title. Private by design: see
77+
locked phone screen — no longer drops the answer. Starting a session from the
78+
phone takes an optional title — or leave it blank and the first substance you
79+
log names it. Rename a session or entry right from the phone by tapping its
80+
title. Private by design: see
7781
[Architecture](#architecture) for how.
7882
- **Substance catalogue & log** — keep your own substance list with notes, and
7983
review your history grouped by substance.

ROADMAP.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,29 @@ non-negotiables, each with a test:
123123

124124
---
125125

126+
## Shipped in v0.11.3
127+
128+
- **An untitled session is named after its first dose.** The phone starts a
129+
session with a single tap and had nowhere to put a title, so phone-started
130+
entries lived in the journal as "Untitled" — the one thing that makes a journal
131+
hard to read back. `name_after_first_dose` (in `db.rs`, called from `log_dose`)
132+
fills a blank title with the substance name of the session's **first** dose.
133+
Deliberate limits: only the first dose (after that the session has a name, and a
134+
title cleared back to blank was cleared on purpose), never over a title the user
135+
typed, and renaming still wins. It sits at the db layer, so it holds for the
136+
desktop, the phone, and the Companion's `log_dose` tool alike.
137+
- The desktop's "+ Session" form used to stamp the literal string
138+
`"Untitled experience"` into the title column when the field was left blank,
139+
which would have made the auto-name unreachable there. It now stores a blank
140+
title and leans on the display fallback the journal already had.
141+
- The phone's start-a-session pane gained an **optional** title field. Offered,
142+
never demanded: typing a title is often the last thing you want to be doing at
143+
the moment you're starting a session.
144+
- **Still untitled:** a session that is started and never gets a dose. Nothing to
145+
name it after; left alone rather than guessed at.
146+
147+
---
148+
126149
## Shipped in v0.11.0
127150

128151
- **First-class past-experience logging.** Writing up a trip that already happened

src-tauri/src/db.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ pub fn log_dose(conn: &Connection, input: &DoseInput) -> rusqlite::Result<(Dose,
510510
})
511511
})?;
512512

513+
name_after_first_dose(conn, input.experience_id, &input.substance_name)?;
514+
513515
// Gather every distinct substance in this experience and check interactions.
514516
let mut stmt =
515517
conn.prepare("SELECT DISTINCT substance_name FROM doses WHERE experience_id = ?1")?;
@@ -519,6 +521,33 @@ pub fn log_dose(conn: &Connection, input: &DoseInput) -> rusqlite::Result<(Dose,
519521
Ok((dose, combo_warnings(conn, &names)))
520522
}
521523

524+
/// Give an untitled session the name of the first substance logged into it.
525+
///
526+
/// Starting a session mid-stride — especially from the phone, one-handed — is a
527+
/// single tap with nowhere to type a title, and the entry would otherwise land in
528+
/// the journal as "Untitled" forever. The first dose is the most useful thing we
529+
/// know about a session, so it becomes the title.
530+
///
531+
/// Deliberately only the *first* dose: after that the session has a name, and a
532+
/// user who clears the title back to blank meant to, so nothing refills it.
533+
/// Renaming (desktop or phone) overrides this at any point.
534+
fn name_after_first_dose(conn: &Connection, experience_id: i64, substance: &str) -> rusqlite::Result<()> {
535+
let name = substance.trim();
536+
if name.is_empty() {
537+
return Ok(());
538+
}
539+
let doses: i64 =
540+
conn.query_row("SELECT COUNT(*) FROM doses WHERE experience_id = ?1", [experience_id], |r| r.get(0))?;
541+
if doses != 1 {
542+
return Ok(());
543+
}
544+
conn.execute(
545+
"UPDATE experiences SET title = ?2 WHERE id = ?1 AND TRIM(COALESCE(title, '')) = ''",
546+
params![experience_id, name],
547+
)?;
548+
Ok(())
549+
}
550+
522551
/// Every warning we know about for a set of substances taken together, from both
523552
/// sources: the class-rule backstop in `interactions.rs` and DoseWiki's graded
524553
/// interaction lists. The most severe warning per pair survives.
@@ -887,6 +916,49 @@ mod tests {
887916
assert_eq!(detail.doses.len(), 2);
888917
}
889918

919+
#[test]
920+
fn an_untitled_session_takes_the_name_of_its_first_dose() {
921+
let c = mem();
922+
let blank = |started_at: &str, title: &str| {
923+
create_experience(&c, &ExperienceInput {
924+
kind: "session".into(), title: title.into(),
925+
intention: String::new(), setting: String::new(),
926+
started_at: started_at.into(),
927+
}).unwrap()
928+
};
929+
let dose = |exp: i64, name: &str, at: &str| {
930+
log_dose(&c, &DoseInput {
931+
experience_id: exp, substance_name: name.into(), amount: Some(100.0),
932+
unit: "mg".into(), route: "oral".into(), taken_at: at.into(),
933+
note: String::new(),
934+
}).unwrap()
935+
};
936+
937+
// Started from the phone with nowhere to type a title: the first dose names it.
938+
let auto = blank("2026-08-01T20:00:00Z", "");
939+
assert_eq!(auto.title, "");
940+
dose(auto.id, "ketamine", "2026-08-01T20:10:00Z");
941+
assert_eq!(get_experience_row(&c, auto.id).unwrap().title, "ketamine");
942+
943+
// The second dose does not rename it — only the first one names a session.
944+
dose(auto.id, "MDMA", "2026-08-01T21:00:00Z");
945+
assert_eq!(get_experience_row(&c, auto.id).unwrap().title, "ketamine");
946+
947+
// …and a title the user typed is never overwritten.
948+
let named = blank("2026-08-02T20:00:00Z", "Birthday");
949+
dose(named.id, "LSD", "2026-08-02T20:10:00Z");
950+
assert_eq!(get_experience_row(&c, named.id).unwrap().title, "Birthday");
951+
952+
// A cleared title stays cleared: that was a decision, not a gap to fill.
953+
update_experience(&c, auto.id, &ExperienceUpdate {
954+
title: String::new(), intention: String::new(), setting: String::new(),
955+
notes: String::new(), rating: None,
956+
started_at: "2026-08-01T20:00:00Z".into(), ended_at: None,
957+
}).unwrap();
958+
dose(auto.id, "caffeine", "2026-08-01T22:00:00Z");
959+
assert_eq!(get_experience_row(&c, auto.id).unwrap().title, "");
960+
}
961+
890962
#[test]
891963
fn timeline_events_can_be_edited_in_place() {
892964
let c = mem();

src/routes/+page.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,11 @@
817817
async function submitNewExperience() {
818818
const startIso = localInputToIso(neStart);
819819
const e = await createExperience({
820-
title: neTitle || "Untitled experience",
820+
// Blank stays blank rather than being stamped with the literal string
821+
// "Untitled experience": an empty title is what lets the first logged dose
822+
// name the session (`name_after_first_dose` in db.rs). The journal already
823+
// falls back to "Untitled experience" for display.
824+
title: neTitle.trim(),
821825
intention: neIntention,
822826
setting: neSetting,
823827
started_at: startIso,
@@ -1866,7 +1870,11 @@
18661870

18671871
{#if showNewExp}
18681872
<div class="new-exp">
1869-
<input placeholder="Title" bind:value={neTitle} />
1873+
<input
1874+
placeholder="Title (optional — else the first substance)"
1875+
title="Leave blank and the session takes the name of the first substance you log."
1876+
bind:value={neTitle}
1877+
/>
18701878
<input type="datetime-local" bind:value={neStart} title={nePast ? "When it started" : "Start time"} />
18711879
{#if nePast}
18721880
<input type="datetime-local" bind:value={neEnd} title="When it ended (optional)" />

src/routes/m/+page.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@
8989
let evNote = $state("");
9090
let evIntensity = $state("");
9191
92+
// starting a session: an optional title, blank by default
93+
let newTitle = $state("");
94+
9295
// renaming an experience (live session or an opened journal entry)
9396
let renamingId = $state<number | null>(null);
9497
let renameText = $state("");
@@ -239,9 +242,14 @@
239242
}
240243
241244
// ---- the live session ----
245+
// A title is offered but never demanded: typing one at the moment you're
246+
// starting a session is often the last thing you want to be doing. Left blank,
247+
// the backend names the session after the first substance logged into it
248+
// (`name_after_first_dose` in db.rs) rather than leaving it "Untitled" forever.
242249
const startSession = () =>
243250
run(async () => {
244-
await createExperience({ title: "", started_at: new Date().toISOString() });
251+
await createExperience({ title: newTitle.trim(), started_at: new Date().toISOString() });
252+
newTitle = "";
245253
await refresh();
246254
});
247255
@@ -571,6 +579,8 @@
571579
<section class="pane">
572580
<h2>No session running</h2>
573581
<p class="muted">Start one here, or carry on with one you started at the desk.</p>
582+
<input placeholder="Title (optional)" bind:value={newTitle} />
583+
<p class="muted hint">Leave it blank and it takes the name of the first substance you log.</p>
574584
<button class="primary" disabled={busy} onclick={startSession}>Start a session</button>
575585
<button disabled={busy} onclick={refresh}>Refresh</button>
576586
</section>
@@ -922,6 +932,8 @@
922932
header { display: flex; justify-content: space-between; align-items: baseline; padding: 0.4rem 0.2rem 0.8rem; }
923933
.live { color: #7ee787; font-size: 0.85rem; }
924934
.muted { color: #9aa2ad; }
935+
/* Sits under the field it explains, so it reads as part of it. */
936+
.hint { font-size: 0.85rem; margin: -0.2rem 0 0.6rem; }
925937
926938
.pane { background: #1b1e24; border: 1px solid #2a2f38; border-radius: 14px; padding: 1rem; margin-bottom: 0.8rem; }
927939
h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }

0 commit comments

Comments
 (0)