Skip to content

Commit 0a28519

Browse files
edwinhuclaude
andcommitted
fix: pass threadId to reply functions for proper threading
The reply functions (replyToThread, replyAllToThread, forwardThread) were receiving threadId but not using it. This caused syncThreadId to fail when threadPane.threadId wasn't already set, leading to "Failed to create reply" errors and fallback to draft mode. Changes: - Add setThreadForReply() to explicitly set thread context - Update openComposeWithCommand() to accept optional threadId - Pass threadId from reply.ts to openReplyCompose/openReplyAllCompose/openForwardCompose Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a75d016 commit 0a28519

2 files changed

Lines changed: 68 additions & 17 deletions

File tree

src/reply.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,18 @@ async function completeDraft(
4646
* threading (threadId, inReplyTo, references), recipients, and subject.
4747
*
4848
* @param conn - The Superhuman connection
49-
* @param _threadId - The thread ID (must be the currently open thread; unused as native command uses current thread)
49+
* @param threadId - The thread ID to reply to
5050
* @param body - The reply body text
5151
* @param send - If true, send immediately; if false, save as draft
5252
* @returns Result with success status and optional draft ID
5353
*/
5454
export async function replyToThread(
5555
conn: SuperhumanConnection,
56-
_threadId: string,
56+
threadId: string,
5757
body: string,
5858
send: boolean = false
5959
): Promise<ReplyResult> {
60-
const draftKey = await openReplyCompose(conn);
60+
const draftKey = await openReplyCompose(conn, threadId);
6161
if (!draftKey) {
6262
return { success: false };
6363
}
@@ -78,18 +78,18 @@ export async function replyToThread(
7878
* and subject automatically.
7979
*
8080
* @param conn - The Superhuman connection
81-
* @param _threadId - The thread ID (must be the currently open thread; unused as native command uses current thread)
81+
* @param threadId - The thread ID to reply to
8282
* @param body - The reply body text
8383
* @param send - If true, send immediately; if false, save as draft
8484
* @returns Result with success status and optional draft ID
8585
*/
8686
export async function replyAllToThread(
8787
conn: SuperhumanConnection,
88-
_threadId: string,
88+
threadId: string,
8989
body: string,
9090
send: boolean = false
9191
): Promise<ReplyResult> {
92-
const draftKey = await openReplyAllCompose(conn);
92+
const draftKey = await openReplyAllCompose(conn, threadId);
9393
if (!draftKey) {
9494
return { success: false };
9595
}
@@ -109,20 +109,20 @@ export async function replyAllToThread(
109109
* the forwarded message content, subject, and formatting.
110110
*
111111
* @param conn - The Superhuman connection
112-
* @param _threadId - The thread ID (must be the currently open thread; unused as native command uses current thread)
112+
* @param threadId - The thread ID to forward
113113
* @param toEmail - The email address to forward to
114114
* @param body - The message body to include before the forwarded content
115115
* @param send - If true, send immediately; if false, save as draft
116116
* @returns Result with success status and optional draft ID
117117
*/
118118
export async function forwardThread(
119119
conn: SuperhumanConnection,
120-
_threadId: string,
120+
threadId: string,
121121
toEmail: string,
122122
body: string,
123123
send: boolean = false
124124
): Promise<ReplyResult> {
125-
const draftKey = await openForwardCompose(conn);
125+
const draftKey = await openForwardCompose(conn, threadId);
126126
if (!draftKey) {
127127
return { success: false };
128128
}

src/superhuman-api.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -455,10 +455,18 @@ async function closeExistingCompose(conn: SuperhumanConnection): Promise<void> {
455455
*/
456456
async function openComposeWithCommand(
457457
conn: SuperhumanConnection,
458-
commandId: string
458+
commandId: string,
459+
threadId?: string
459460
): Promise<string | null> {
460461
await closeExistingCompose(conn);
461-
await syncThreadId(conn);
462+
463+
// If a specific threadId is provided, set it before invoking the command
464+
if (threadId) {
465+
await setThreadForReply(conn, threadId);
466+
await new Promise((r) => setTimeout(r, 300)); // Give UI time to update
467+
} else {
468+
await syncThreadId(conn);
469+
}
462470

463471
const invoked = await invokeCommand(conn, commandId);
464472
if (!invoked) {
@@ -469,6 +477,40 @@ async function openComposeWithCommand(
469477
return getDraftKey(conn);
470478
}
471479

480+
/**
481+
* Set a specific thread ID in the ViewState for reply operations
482+
*
483+
* This sets both threadPane.threadId and threadListView.threadId to ensure
484+
* the reply commands work correctly regardless of what's currently visible.
485+
*/
486+
export async function setThreadForReply(conn: SuperhumanConnection, threadId: string): Promise<boolean> {
487+
const { Runtime } = conn;
488+
489+
const result = await Runtime.evaluate({
490+
expression: `
491+
(() => {
492+
try {
493+
const tree = window.ViewState?.tree;
494+
if (!tree?.set) return false;
495+
496+
const targetThreadId = ${JSON.stringify(threadId)};
497+
498+
// Set both threadPane and threadListView to the target thread
499+
tree.set(['threadPane', 'threadId'], targetThreadId);
500+
tree.set(['threadListView', 'threadId'], targetThreadId);
501+
502+
return true;
503+
} catch (e) {
504+
return false;
505+
}
506+
})()
507+
`,
508+
returnByValue: true,
509+
});
510+
511+
return result.result.value === true;
512+
}
513+
472514
/**
473515
* Sync threadListView.threadId with threadPane.threadId
474516
*
@@ -507,27 +549,36 @@ export async function syncThreadId(conn: SuperhumanConnection): Promise<boolean>
507549
*
508550
* This uses the REPLY_ALL_POP_OUT command which properly sets up threading,
509551
* recipients, and subject automatically.
552+
*
553+
* @param conn - The Superhuman connection
554+
* @param threadId - Optional thread ID to reply to (if not provided, uses current thread)
510555
*/
511-
export async function openReplyAllCompose(conn: SuperhumanConnection): Promise<string | null> {
512-
return openComposeWithCommand(conn, "REPLY_ALL_POP_OUT");
556+
export async function openReplyAllCompose(conn: SuperhumanConnection, threadId?: string): Promise<string | null> {
557+
return openComposeWithCommand(conn, "REPLY_ALL_POP_OUT", threadId);
513558
}
514559

515560
/**
516561
* Open a reply compose using Superhuman's native command.
517562
*
518563
* This uses the REPLY_POP_OUT command which properly sets up threading,
519564
* recipient (original sender only), and subject automatically.
565+
*
566+
* @param conn - The Superhuman connection
567+
* @param threadId - Optional thread ID to reply to (if not provided, uses current thread)
520568
*/
521-
export async function openReplyCompose(conn: SuperhumanConnection): Promise<string | null> {
522-
return openComposeWithCommand(conn, "REPLY_POP_OUT");
569+
export async function openReplyCompose(conn: SuperhumanConnection, threadId?: string): Promise<string | null> {
570+
return openComposeWithCommand(conn, "REPLY_POP_OUT", threadId);
523571
}
524572

525573
/**
526574
* Open a forward compose using Superhuman's native command.
527575
*
528576
* This uses the FORWARD_POP_OUT command which properly sets up the
529577
* forwarded message content and subject automatically.
578+
*
579+
* @param conn - The Superhuman connection
580+
* @param threadId - Optional thread ID to forward (if not provided, uses current thread)
530581
*/
531-
export async function openForwardCompose(conn: SuperhumanConnection): Promise<string | null> {
532-
return openComposeWithCommand(conn, "FORWARD_POP_OUT");
582+
export async function openForwardCompose(conn: SuperhumanConnection, threadId?: string): Promise<string | null> {
583+
return openComposeWithCommand(conn, "FORWARD_POP_OUT", threadId);
533584
}

0 commit comments

Comments
 (0)