feat: bulk-add messages from a pasted dialogue script#21
Conversation
Roadmap Phase 2, item #14. - New parseScript utility: one 'Name: message' per line; Me/You/I (any case) map to sent messages, other speakers to the contact; speakerless lines continue the previous message; timestamps advance one minute per message from a start time, wrapping past midnight. - Bulk Add card in the control panel: script textarea, 'Add to chat' button, and a 'Replace existing' checkbox guarded by a confirm. - Unit tests for the parser (7) and UI tests for append, replace, and empty-script no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SN53MtH3hvbHr7syhwu54q
There was a problem hiding this comment.
Code Review
This pull request introduces a 'Bulk Add' feature that allows users to paste a dialogue script and parse it into chat messages. It includes a new script parser utility, corresponding unit tests, and UI integration in the control panel with support for appending or replacing existing messages. The review feedback highlights a bug where lines with a speaker prefix but no content are incorrectly parsed, suggests adding a unit test to prevent regressions for this case, and recommends passing the user's current timestamp to the parser instead of relying on a hardcoded default start time.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const match = LINE_RE.exec(line) | ||
| if (match && match[2].trim()) { | ||
| const speaker = match[1].trim() | ||
| const sender = ME_ALIASES.has(speaker.toLowerCase()) ? "me" : "them" | ||
| messages.push({ | ||
| text: match[2].trim(), | ||
| sender, | ||
| timestamp: formatTime(minutes), | ||
| status: "read", | ||
| }) | ||
| minutes += 1 |
There was a problem hiding this comment.
There is a bug when parsing lines that contain a speaker prefix but no message content (e.g., "Me: " or "Sarah: ").
Because match[2].trim() is empty, the condition match && match[2].trim() evaluates to false. The parser then falls into the else if or else blocks:
- If there are existing messages, it appends the trimmed line (e.g.,
"Me:") to the previous message as a continuation. - If there are no existing messages, it treats the line as a speakerless leading line and adds a message with the text
"Me:"sent by the contact ("them").
We should instead ignore lines that have a speaker prefix but no message content.
| const match = LINE_RE.exec(line) | |
| if (match && match[2].trim()) { | |
| const speaker = match[1].trim() | |
| const sender = ME_ALIASES.has(speaker.toLowerCase()) ? "me" : "them" | |
| messages.push({ | |
| text: match[2].trim(), | |
| sender, | |
| timestamp: formatTime(minutes), | |
| status: "read", | |
| }) | |
| minutes += 1 | |
| const match = LINE_RE.exec(line) | |
| if (match) { | |
| const text = match[2].trim() | |
| if (!text) continue | |
| const speaker = match[1].trim() | |
| const sender = ME_ALIASES.has(speaker.toLowerCase()) ? "me" : "them" | |
| messages.push({ | |
| text, | |
| sender, | |
| timestamp: formatTime(minutes), | |
| status: "read", | |
| }) | |
| minutes += 1 |
|
|
||
| it("returns an empty list for empty input", () => { | ||
| expect(parseScript("")).toEqual([]) | ||
| expect(parseScript(" \n ")).toEqual([]) | ||
| }) |
There was a problem hiding this comment.
To prevent regressions, we should add a unit test that verifies lines with a speaker prefix but no message content are correctly ignored.
it("ignores lines with a speaker prefix but no message content", () => {
const msgs = parseScript("Me: hello\nSarah: \nMe: world")
expect(msgs).toHaveLength(2)
expect(msgs[0].text).toBe("hello")
expect(msgs[1].text).toBe("world")
})
it("returns an empty list for empty input", () => {
expect(parseScript("")).toEqual([])
expect(parseScript(" \n ")).toEqual([])
})| const handleBulkAdd = () => { | ||
| const parsed = parseScript(script) |
There was a problem hiding this comment.
Currently, parseScript is called without the second argument, meaning bulk-added messages will always start at the hardcoded default of "10:30".
We can make this dynamic and respect the user's preferred time by passing the existing timestamp state (from the message form) as the startTime parameter.
| const handleBulkAdd = () => { | |
| const parsed = parseScript(script) | |
| const handleBulkAdd = () => { | |
| const parsed = parseScript(script, timestamp) |
Address review feedback on #21: - A speaker prefix with no content ('Sarah: ') was glued onto the previous message (or became a bogus leading message); such lines are now skipped, with a regression test. - Bulk-added messages now start their timestamps from the compose form's Time field instead of the hardcoded 10:30 default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SN53MtH3hvbHr7syhwu54q
Summary
Implements Roadmap Phase 2, item #14 — bulk/script entry, the roadmap's "huge time-saver for creators": paste a whole dialogue and build the thread in one click instead of composing message by message.
Based directly on
main— first PR of the post-merge cycle, with CI now active.What changed
src/script-parser.ts—parseScript(): oneName: messageper line;Me/You/I(any case) become sent messages, any other speaker becomes the contact; speakerless lines continue the previous message on a new line; timestamps advance one minute per message from a start time (wrapping past midnight); malformed start times fall back to the default.Verification
pnpm test— 48/48 pass across 2 files (7 new parser unit tests + 3 new UI tests: append, replace, empty-script no-op)pnpm lint— cleanpnpm build— succeedsNotes
.txtimport-export, then shareable URL state.🤖 Generated with Claude Code
Generated by Claude Code