Skip to content

Commit 86680ef

Browse files
Ameclaude
authored andcommitted
feat(workspaces): quick-chat is one chat workspace per day
Key quick-chat find-or-create on TODAY's daily tag (`chat-<mon><day>`, e.g. `chat-jun15`) instead of "the most-recent chat-template workspace". Enter today's workspace if it exists, else create it; each send is a new session inside today's workspace (conversations = sessions, resumable from the chat sidebar). Traditional-chatbot feel, aligned with Workspace=daily-container / Session=conversation. The daily tag mirrors the frontend's `defaultTagFor` byte-for-byte (en-US short month, lowercased) so a quick-chat-created daily workspace and a form-created one on the same day converge on the same workspace instead of duplicating. Replaces the prior `chat` / `chat-2` fallback tag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c65cb2b commit 86680ef

1 file changed

Lines changed: 40 additions & 30 deletions

File tree

src/webui/routes/workspaces.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,19 @@ const MAX_SEED_PROMPT = 16000;
4646
/** The template quick-chat reuses-or-creates its workspace from. */
4747
const QUICK_CHAT_TEMPLATE = 'chat';
4848

49+
const MONTH_ABBR = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] as const;
50+
4951
/**
50-
* A valid, unused tag for the one chat workspace quick-chat creates on first
51-
* use (`chat`, then `chat-2`, … on collision). Quick-chat reuses this workspace
52-
* thereafter, so this only runs once until the user deletes it.
52+
* Tag for TODAY's chat workspace — `chat-<mon><day>` (e.g. `chat-jun15`).
53+
* Quick-chat is one-workspace-per-DAY: today's conversations are sessions inside
54+
* today's workspace. The format mirrors the frontend's `defaultTagFor`
55+
* (`<template>-<month><day>`, en-US short month lowercased) so a quick-chat-
56+
* created daily workspace is byte-identical to one created from the form on the
57+
* same day — the two converge on the same workspace instead of duplicating.
5358
*/
54-
function freshChatTag(svc: WorkspaceService): string {
55-
if (!svc.registry.hasTag(QUICK_CHAT_TEMPLATE)) return QUICK_CHAT_TEMPLATE;
56-
for (let i = 2; i < 1000; i++) {
57-
const t = `${QUICK_CHAT_TEMPLATE}-${i}`;
58-
if (!svc.registry.hasTag(t)) return t;
59-
}
60-
return `${QUICK_CHAT_TEMPLATE}-${randomUUID().slice(0, 8)}`;
59+
function todayChatTag(): string {
60+
const now = new Date();
61+
return `${QUICK_CHAT_TEMPLATE}-${MONTH_ABBR[now.getMonth()]}${now.getDate()}`;
6162
}
6263

6364
/**
@@ -187,33 +188,41 @@ export function createWorkspaceRoutes(svc: WorkspaceService): Hono {
187188
}
188189
}
189190

190-
// Serializes quick-chat's find-or-create so two concurrent FIRST-TIME launches
191-
// don't both bootstrap a `chat` workspace — the loser's `registry.add` would
192-
// throw on the duplicate tag and leak an orphaned bootstrap dir. In-process
193-
// chain (quick-chat is low-frequency, single-process); the `.catch` keeps a
194-
// failed run from poisoning the gate forever.
191+
// TODAY's chat workspace, by its daily tag (`chat-jun15`). A workspace someone
192+
// happened to tag `chat-jun15` with a non-chat template doesn't count — the
193+
// daily bucket is a chat-template workspace.
194+
const findTodaysChat = (): WorkspaceMeta | undefined => {
195+
const tag = todayChatTag();
196+
return svc.registry.list().find((w) => w.template === QUICK_CHAT_TEMPLATE && w.tag === tag);
197+
};
198+
199+
// Serializes quick-chat's find-or-create so two concurrent FIRST-OF-DAY
200+
// launches don't both bootstrap today's workspace — the loser's `registry.add`
201+
// would throw on the duplicate tag and leak an orphaned bootstrap dir.
202+
// In-process chain (quick-chat is low-frequency, single-process); the `.catch`
203+
// keeps a failed run from poisoning the gate forever.
195204
let chatWsGate: Promise<unknown> = Promise.resolve();
196205

197206
const findOrCreateChatWorkspace = async (): Promise<
198207
{ ok: true; meta: WorkspaceMeta } | { ok: false; status: number; body: { error: string; message?: string } }
199208
> => {
200-
const existing = svc.registry.list().find((w) => w.template === QUICK_CHAT_TEMPLATE);
209+
const existing = findTodaysChat();
201210
if (existing) return { ok: true, meta: existing };
202211
let created: Awaited<ReturnType<typeof svc.creator.create>>;
203212
try {
204-
created = await svc.creator.create(freshChatTag(svc), QUICK_CHAT_TEMPLATE);
213+
created = await svc.creator.create(todayChatTag(), QUICK_CHAT_TEMPLATE);
205214
} catch (err) {
206-
// e.g. a concurrent create committed the tag between freshChatTag() and
207-
// registry.add(). Re-find — the winner's workspace now exists.
208-
const after = svc.registry.list().find((w) => w.template === QUICK_CHAT_TEMPLATE);
215+
// e.g. a concurrent create committed today's tag first. Re-find — the
216+
// winner's workspace now exists.
217+
const after = findTodaysChat();
209218
if (after) return { ok: true, meta: after };
210219
launcherLogger.error('quick_chat.create_threw', { err });
211220
return { ok: false, status: 500, body: { error: 'create_failed', message: (err as Error).message } };
212221
}
213222
if (!created.ok) {
214-
// tag_in_use means someone else created a chat workspace — re-find and reuse.
223+
// tag_in_use means today's workspace was created concurrently — re-find it.
215224
if (created.code === 'tag_in_use') {
216-
const after = svc.registry.list().find((w) => w.template === QUICK_CHAT_TEMPLATE);
225+
const after = findTodaysChat();
217226
if (after) return { ok: true, meta: after };
218227
}
219228
const status =
@@ -478,8 +487,8 @@ export function createWorkspaceRoutes(svc: WorkspaceService): Hono {
478487
});
479488

480489
// Quick-chat launch — the "type a message → you're in" front door, decoupled
481-
// from the multi-step create-workspace UI. Reuses (or, on first use, creates)
482-
// the chat-template workspace, then spawns ONE fresh interactive session
490+
// from the multi-step create-workspace UI. Enters TODAY's chat workspace
491+
// (creating it on the day's first use), then spawns a fresh interactive session
483492
// seeded with the user's first message. One POST returns both the workspace
484493
// and the live session, so the client can drop the user straight into the TUI.
485494
// Body: { prompt: string; agent?: string }
@@ -499,12 +508,13 @@ export function createWorkspaceRoutes(svc: WorkspaceService): Hono {
499508
return c.json({ error: 'bad_request', message: (err as Error).message }, 400);
500509
}
501510

502-
// Reuse the most-recent chat-template workspace; create one only if none
503-
// exists. Create is heavy (bash + git + skill injection), so we never make a
504-
// fresh workspace per message — the codebase's "heavy create once, cheap
505-
// spawn many" pattern (sessions, not workspaces, carry conversations). The
506-
// find-or-create runs through `chatWsGate` so concurrent first launches
507-
// don't double-bootstrap.
511+
// One chat workspace per DAY: enter today's if it exists, else create it.
512+
// Each send is a new SESSION inside today's workspace (conversations =
513+
// sessions, resumable from the chat sidebar) — closer to a traditional
514+
// chatbot while staying aligned with the Workspace/Session model. Create is
515+
// heavy (bash + git + skill injection) but happens at most once per day. The
516+
// find-or-create runs through `chatWsGate` so concurrent first-of-day
517+
// launches don't double-bootstrap.
508518
const run = chatWsGate.catch(() => undefined).then(() => findOrCreateChatWorkspace());
509519
chatWsGate = run;
510520
const target = await run;

0 commit comments

Comments
 (0)