generated from coulomb/repo-seed
Auto-generate session name when user creates without typing one
Click "Create session" with the input empty and a name of the form `YYMMDD-session-NNN` is generated automatically: today's date as two-digit year/month/day, then a zero-padded counter that starts at 000 and increments past the highest existing match for the same day. Added: - `computeNextDefaultName(existing, now?)` pure helper exported from `@engine/services/sessions`. - `SessionService.nextDefaultName(now?)` method that wraps it against the current repo. - Both create call sites (CreateFirstSession empty state + SessionMenu's New session form) fall back to `service.nextDefaultName()` when the trimmed input is empty. - 5 new unit tests covering today-only counting, max-not-count increment, and trimmed/wrong-shape filtering. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,9 @@ export function CreateFirstSession() {
|
||||
const handleCreate = useCallback(() => {
|
||||
setError(null);
|
||||
try {
|
||||
const created = service.create(name);
|
||||
const trimmed = name.trim();
|
||||
const effective = trimmed.length === 0 ? service.nextDefaultName() : name;
|
||||
const created = service.create(effective);
|
||||
navigateTo({ sessionId: created.id, mode: "review" });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
|
||||
@@ -104,7 +104,9 @@ export function SessionMenu({ onExportZip, onImportZip, onOpenSamples }: Session
|
||||
const handleCreate = useCallback(() => {
|
||||
setError(null);
|
||||
try {
|
||||
const created = service.create(newName);
|
||||
const trimmed = newName.trim();
|
||||
const effective = trimmed.length === 0 ? service.nextDefaultName() : newName;
|
||||
const created = service.create(effective);
|
||||
setNewName("");
|
||||
setCreating(false);
|
||||
setOpen(false);
|
||||
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
ACTIVE_SESSION_KEY,
|
||||
attachSessionPersister,
|
||||
clearAllSessionData,
|
||||
computeNextDefaultName,
|
||||
createSessionService,
|
||||
DuplicateSessionNameError,
|
||||
engineSnapshotKey,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
ACTIVE_SESSION_KEY,
|
||||
attachSessionPersister,
|
||||
computeNextDefaultName,
|
||||
createSessionService,
|
||||
DuplicateSessionNameError,
|
||||
engineSnapshotKey,
|
||||
@@ -50,6 +51,51 @@ describe("engineSnapshotKey", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeNextDefaultName", () => {
|
||||
const now = new Date(2026, 4, 27); // 2026-05-27 → "260527"
|
||||
|
||||
it("returns 000 for the first session of the day", () => {
|
||||
expect(computeNextDefaultName([], now)).toBe("260527-session-000");
|
||||
});
|
||||
|
||||
it("counts only sessions whose name matches today's prefix", () => {
|
||||
const existing = [
|
||||
{ name: "260527-session-000" } as Session,
|
||||
{ name: "260527-session-001" } as Session,
|
||||
{ name: "Random other name" } as Session,
|
||||
{ name: "260526-session-007" } as Session, // yesterday — ignored
|
||||
];
|
||||
expect(computeNextDefaultName(existing, now)).toBe("260527-session-002");
|
||||
});
|
||||
|
||||
it("increments past the highest, not the count", () => {
|
||||
const existing = [
|
||||
{ name: "260527-session-005" } as Session,
|
||||
{ name: "260527-session-002" } as Session,
|
||||
];
|
||||
expect(computeNextDefaultName(existing, now)).toBe("260527-session-006");
|
||||
});
|
||||
|
||||
it("ignores names with the wrong shape and accepts trimmed matches", () => {
|
||||
const existing = [
|
||||
{ name: " 260527-session-003 " } as Session, // whitespace OK
|
||||
{ name: "260527-session-12" } as Session, // wrong digit count
|
||||
{ name: "260527-sess-005" } as Session, // wrong infix
|
||||
];
|
||||
expect(computeNextDefaultName(existing, now)).toBe("260527-session-004");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SessionService.nextDefaultName", () => {
|
||||
it("delegates to computeNextDefaultName against the current repo", () => {
|
||||
const s = freshService();
|
||||
const now = new Date(2026, 4, 27);
|
||||
s.service.create({ name: "260527-session-000" });
|
||||
s.service.create({ name: "260527-session-001" });
|
||||
expect(s.service.nextDefaultName(now)).toBe("260527-session-002");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SessionService — lifecycle", () => {
|
||||
let s: ReturnType<typeof freshService>;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -68,6 +68,12 @@ export interface SessionService {
|
||||
getActive(): SessionId | null;
|
||||
/** Record an "I just opened this" timestamp on the session. */
|
||||
touch(id: SessionId): Session | null;
|
||||
/**
|
||||
* Suggest a default session name when the user hasn't typed one.
|
||||
* Returns `YYMMDD-session-NNN` where `NNN` is the next free counter
|
||||
* for today (starting at `000`). Pure: does not mutate the repo.
|
||||
*/
|
||||
nextDefaultName(now?: Date): string;
|
||||
}
|
||||
|
||||
function nowIso(now?: string): string {
|
||||
@@ -189,9 +195,39 @@ export function createSessionService(
|
||||
if (!existing) return null;
|
||||
return repo.update({ ...existing, lastOpenedAt: nowIso() });
|
||||
},
|
||||
nextDefaultName(now = new Date()) {
|
||||
return computeNextDefaultName(repo.list(), now);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure helper exported for tests + callers that want to preview the
|
||||
* name without going through the service instance. Format:
|
||||
* `YYMMDD-session-NNN`. `NNN` increments only against today's
|
||||
* existing sessions; tomorrow's counter starts fresh at `000`.
|
||||
*/
|
||||
export function computeNextDefaultName(
|
||||
existing: readonly Session[],
|
||||
now: Date = new Date(),
|
||||
): string {
|
||||
const yy = String(now.getFullYear() % 100).padStart(2, "0");
|
||||
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(now.getDate()).padStart(2, "0");
|
||||
const prefix = `${yy}${mm}${dd}-session-`;
|
||||
const re = new RegExp(`^${prefix}(\\d{3})$`);
|
||||
let max = -1;
|
||||
for (const s of existing) {
|
||||
const m = re.exec(s.name.trim());
|
||||
if (m) {
|
||||
const n = parseInt(m[1]!, 10);
|
||||
if (n > max) max = n;
|
||||
}
|
||||
}
|
||||
const next = String(max + 1).padStart(3, "0");
|
||||
return `${prefix}${next}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-session persistence (the session index + active-session pointer).
|
||||
// Per-session engine snapshots are handled by `attachPersister` against
|
||||
|
||||
Reference in New Issue
Block a user