Skip to content

Commit c30f4ec

Browse files
MarkShawn2020claude
andcommitted
feat(chat): add copy resume command and fix project path decoding
- Add "Copy Resume Command" menu item that copies `cd <path> && claude --resume <id>` - Extract cwd from session JSONL for accurate project paths (fixes hyphenated dir names) - "Copy Session ID" now always visible (not gated behind onResume) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9cad908 commit c30f4ec

6 files changed

Lines changed: 75 additions & 31 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ struct RawLine {
236236
summary: Option<String>,
237237
slug: Option<String>,
238238
uuid: Option<String>,
239+
cwd: Option<String>,
239240
message: Option<RawMessage>,
240241
timestamp: Option<String>,
241242
#[serde(rename = "isMeta")]
@@ -649,6 +650,7 @@ async fn get_sessions_usage(project_id: String) -> Result<Vec<SessionUsageEntry>
649650
struct SessionHead {
650651
title: Option<String>,
651652
summary: Option<String>,
653+
cwd: Option<String>,
652654
message_count: usize,
653655
}
654656

@@ -672,12 +674,13 @@ fn read_session_head(path: &Path, max_lines: usize) -> SessionHead {
672674

673675
let file = match fs::File::open(path) {
674676
Ok(f) => f,
675-
Err(_) => return SessionHead { title: None, summary: None, message_count: 0 },
677+
Err(_) => return SessionHead { title: None, summary: None, cwd: None, message_count: 0 },
676678
};
677679

678680
let reader = BufReader::new(file);
679681
let mut summary = None;
680682
let mut slug: Option<String> = None;
683+
let mut cwd: Option<String> = None;
681684
let mut first_user_message: Option<String> = None;
682685
let mut message_count = 0;
683686

@@ -700,6 +703,14 @@ fn read_session_head(path: &Path, max_lines: usize) -> SessionHead {
700703
}
701704
if parsed.line_type.as_deref() == Some("user") {
702705
message_count += 1;
706+
// Capture cwd from first user message
707+
if cwd.is_none() {
708+
if let Some(c) = &parsed.cwd {
709+
if !c.is_empty() {
710+
cwd = Some(c.clone());
711+
}
712+
}
713+
}
703714
// Capture first user message as fallback summary
704715
if first_user_message.is_none() {
705716
if let Some(msg) = &parsed.message {
@@ -740,7 +751,7 @@ fn read_session_head(path: &Path, max_lines: usize) -> SessionHead {
740751

741752
let title = slug.map(|s| slug_to_title(&s));
742753
let final_summary = summary.or(first_user_message).map(|s| restore_slash_command(&s));
743-
SessionHead { title, summary: final_summary, message_count }
754+
SessionHead { title, summary: final_summary, cwd, message_count }
744755
}
745756

746757
/// Convert <command-message>...</command-message><command-name>/cmd</command-name> to /cmd format
@@ -867,7 +878,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
867878
.map(|d| d.as_secs())
868879
.unwrap_or(last_modified);
869880

870-
let display_path = decode_project_path(project_id);
881+
let display_path = head.cwd.clone().unwrap_or_else(|| decode_project_path(project_id));
871882

872883
all_sessions.push(Session {
873884
id: session_id.clone(),
@@ -909,6 +920,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
909920
}
910921

911922
let head = read_session_head(&path, 20);
923+
let session_path = head.cwd.clone().unwrap_or_else(|| display_path.clone());
912924

913925
let metadata = fs::metadata(&path).ok();
914926
let last_modified = metadata.as_ref()
@@ -925,7 +937,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
925937
all_sessions.push(Session {
926938
id: session_id,
927939
project_id: project_id.clone(),
928-
project_path: Some(display_path.clone()),
940+
project_path: Some(session_path),
929941
title: head.title,
930942
summary: head.summary,
931943
message_count: head.message_count,
@@ -1014,6 +1026,7 @@ async fn list_all_chats(
10141026
let content = fs::read_to_string(&path).unwrap_or_default();
10151027

10161028
let mut session_summary: Option<String> = None;
1029+
let mut session_cwd: Option<String> = None;
10171030
let mut session_messages: Vec<ChatMessage> = Vec::new();
10181031

10191032
for line in content.lines() {
@@ -1024,6 +1037,15 @@ async fn list_all_chats(
10241037
session_summary = parsed.summary;
10251038
}
10261039

1040+
// Capture cwd from first user message
1041+
if session_cwd.is_none() {
1042+
if let Some(c) = &parsed.cwd {
1043+
if !c.is_empty() {
1044+
session_cwd = Some(c.clone());
1045+
}
1046+
}
1047+
}
1048+
10271049
if line_type == Some("user") || line_type == Some("assistant") {
10281050
if let Some(msg) = &parsed.message {
10291051
let role = msg.role.clone().unwrap_or_default();
@@ -1048,9 +1070,11 @@ async fn list_all_chats(
10481070
}
10491071
}
10501072

1051-
// Update session_summary for all messages
1073+
// Update session_summary and project_path for all messages
1074+
let resolved_path = session_cwd.unwrap_or(project_path);
10521075
for msg in &mut session_messages {
10531076
msg.session_summary = session_summary.clone();
1077+
msg.project_path = resolved_path.clone();
10541078
}
10551079

10561080
all_chats.extend(session_messages);

src/components/GlobalHeader/VerticalFeatureTabs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ function SessionItem({ session, onResume, projectLabel }: SessionItemProps) {
611611
<SessionDropdownMenuItems
612612
projectId={session.project_id}
613613
sessionId={session.id}
614+
projectPath={session.project_path}
614615
onResume={onResume}
615616
/>
616617
</DropdownMenuContent>

src/components/shared/SessionMenuItems.tsx

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invoke } from "@tauri-apps/api/core";
2-
import { FolderOpen, Copy, Download } from "lucide-react";
2+
import { FolderOpen, Copy, Download, Terminal } from "lucide-react";
33
import { ExternalLinkIcon, ChatBubbleIcon } from "@radix-ui/react-icons";
44
import {
55
DropdownMenuItem,
@@ -15,6 +15,7 @@ import {
1515
export interface SessionMenuConfig {
1616
projectId: string;
1717
sessionId: string;
18+
projectPath?: string;
1819
originalChat?: boolean;
1920
setOriginalChat?: (v: boolean) => void;
2021
markdownPreview?: boolean;
@@ -34,39 +35,48 @@ export function useSessionMenuHandlers(projectId: string, sessionId: string) {
3435
await invoke("copy_to_clipboard", { text: path });
3536
};
3637
const handleCopySessionId = () => invoke("copy_to_clipboard", { text: sessionId });
38+
const handleCopyResumeCommand = (projectPath: string) => {
39+
const cmd = `cd ${projectPath} && claude --resume ${sessionId}`;
40+
return invoke("copy_to_clipboard", { text: cmd });
41+
};
3742

38-
return { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId };
43+
return { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId, handleCopyResumeCommand };
3944
}
4045

4146
// DropdownMenu items
4247
export function SessionDropdownMenuItems({
4348
projectId,
4449
sessionId,
50+
projectPath,
4551
originalChat,
4652
setOriginalChat,
4753
markdownPreview,
4854
setMarkdownPreview,
4955
onExport,
5056
onResume,
5157
}: SessionMenuConfig) {
52-
const { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId } =
58+
const { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId, handleCopyResumeCommand } =
5359
useSessionMenuHandlers(projectId, sessionId);
5460

5561
return (
5662
<>
63+
<DropdownMenuItem onClick={handleCopySessionId} className="gap-2">
64+
<Copy size={14} />
65+
Copy Session ID
66+
</DropdownMenuItem>
67+
{projectPath && (
68+
<DropdownMenuItem onClick={() => handleCopyResumeCommand(projectPath)} className="gap-2">
69+
<Terminal size={14} />
70+
Copy Resume Command
71+
</DropdownMenuItem>
72+
)}
5773
{onResume && (
58-
<>
59-
<DropdownMenuItem onClick={handleCopySessionId} className="gap-2">
60-
<Copy size={14} />
61-
Copy Session ID
62-
</DropdownMenuItem>
63-
<DropdownMenuItem onClick={onResume} className="gap-2">
64-
<ChatBubbleIcon className="w-3.5 h-3.5" />
65-
Resume Session
66-
</DropdownMenuItem>
67-
<DropdownMenuSeparator />
68-
</>
74+
<DropdownMenuItem onClick={onResume} className="gap-2">
75+
<ChatBubbleIcon className="w-3.5 h-3.5" />
76+
Resume Session
77+
</DropdownMenuItem>
6978
)}
79+
<DropdownMenuSeparator />
7080
<DropdownMenuItem onClick={handleReveal} className="gap-2">
7181
<FolderOpen size={14} />
7282
Reveal in Finder
@@ -111,31 +121,36 @@ export function SessionDropdownMenuItems({
111121
export function SessionContextMenuItems({
112122
projectId,
113123
sessionId,
124+
projectPath,
114125
originalChat,
115126
setOriginalChat,
116127
markdownPreview,
117128
setMarkdownPreview,
118129
onExport,
119130
onResume,
120131
}: SessionMenuConfig) {
121-
const { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId } =
132+
const { handleReveal, handleOpenInEditor, handleCopyPath, handleCopySessionId, handleCopyResumeCommand } =
122133
useSessionMenuHandlers(projectId, sessionId);
123134

124135
return (
125136
<>
137+
<ContextMenuItem onClick={handleCopySessionId} className="gap-2">
138+
<Copy size={14} />
139+
Copy Session ID
140+
</ContextMenuItem>
141+
{projectPath && (
142+
<ContextMenuItem onClick={() => handleCopyResumeCommand(projectPath)} className="gap-2">
143+
<Terminal size={14} />
144+
Copy Resume Command
145+
</ContextMenuItem>
146+
)}
126147
{onResume && (
127-
<>
128-
<ContextMenuItem onClick={handleCopySessionId} className="gap-2">
129-
<Copy size={14} />
130-
Copy Session ID
131-
</ContextMenuItem>
132-
<ContextMenuItem onClick={onResume} className="gap-2">
133-
<ChatBubbleIcon className="w-3.5 h-3.5" />
134-
Resume Session
135-
</ContextMenuItem>
136-
<ContextMenuSeparator />
137-
</>
148+
<ContextMenuItem onClick={onResume} className="gap-2">
149+
<ChatBubbleIcon className="w-3.5 h-3.5" />
150+
Resume Session
151+
</ContextMenuItem>
138152
)}
153+
<ContextMenuSeparator />
139154
<ContextMenuItem onClick={handleReveal} className="gap-2">
140155
<FolderOpen size={14} />
141156
Reveal in Finder

src/views/Chat/MessageView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export function MessageView({ projectId, projectPath, sessionId, summary: initia
114114
<SessionContextMenuItems
115115
projectId={projectId}
116116
sessionId={sessionId}
117+
projectPath={projectPath}
117118
originalChat={originalChat}
118119
setOriginalChat={setOriginalChat}
119120
markdownPreview={markdownPreview}
@@ -132,6 +133,7 @@ export function MessageView({ projectId, projectPath, sessionId, summary: initia
132133
<SessionDropdownMenuItems
133134
projectId={projectId}
134135
sessionId={sessionId}
136+
projectPath={projectPath}
135137
originalChat={originalChat}
136138
setOriginalChat={setOriginalChat}
137139
markdownPreview={markdownPreview}

src/views/Chat/ProjectList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ function SessionDetail({ session, onClose }: { session: Session; onClose: () =>
542542
<SessionDropdownMenuItems
543543
projectId={session.project_id}
544544
sessionId={session.id}
545+
projectPath={session.project_path}
545546
onExport={() => setExportDialogOpen(true)}
546547
/>
547548
<DropdownMenuSeparator />

src/views/Workspace/ProjectDashboard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ export function ProjectDashboard({ project }: ProjectDashboardProps) {
322322
<SessionDropdownMenuItems
323323
projectId={session.project_id}
324324
sessionId={session.id}
325+
projectPath={session.project_path}
325326
onResume={() => handleResumeSession(session)}
326327
/>
327328
</DropdownMenuContent>

0 commit comments

Comments
 (0)