Skip to content

Commit 8c538a3

Browse files
authored
feat(ui): add cross-navigation between note and session modals (#403)
1 parent c5ed2ba commit 8c538a3

5 files changed

Lines changed: 134 additions & 27 deletions

File tree

apps/staged/src/lib/features/branches/BranchCard.svelte

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@
160160
let commitDiffSha = $state<string | null>(null);
161161
162162
// Note modal (opened by clicking a note in the timeline)
163-
let openNote = $state<{ title: string; content: string } | null>(null);
163+
let openNote = $state<{ title: string; content: string; sessionId?: string } | null>(null);
164164
165165
// Image viewer modal (opened by clicking an image in the timeline)
166166
let viewImageId = $state<string | null>(null);
@@ -351,12 +351,21 @@
351351
// Timeline item interactions
352352
// =========================================================================
353353
354+
/** Look up note info from timeline data by session ID (for cross-modal navigation). */
355+
function findNoteForSession(
356+
sessionId: string
357+
): { id: string; title: string; content: string } | null {
358+
const note = timeline?.notes.find((n) => n.sessionId === sessionId && n.content?.trim());
359+
if (!note) return null;
360+
return { id: note.id, title: note.title, content: note.content };
361+
}
362+
354363
function handleCommitClick(sha: string) {
355364
commitDiffSha = sha;
356365
}
357366
358-
function handleNoteClick(_noteId: string, title: string, content: string) {
359-
openNote = { title, content };
367+
function handleNoteClick(_noteId: string, title: string, content: string, sessionId?: string) {
368+
openNote = { title, content, sessionId };
360369
}
361370
362371
async function handleReviewClick(reviewId: string) {
@@ -803,7 +812,16 @@
803812
{/if}
804813

805814
{#if openNote}
806-
<NoteModal title={openNote.title} content={openNote.content} onClose={() => (openNote = null)} />
815+
<NoteModal
816+
title={openNote.title}
817+
content={openNote.content}
818+
sessionId={openNote.sessionId}
819+
onClose={() => (openNote = null)}
820+
onOpenSession={(sid) => {
821+
openNote = null;
822+
sessionMgr.openSessionId = sid;
823+
}}
824+
/>
807825
{/if}
808826

809827
{#if viewImageId}
@@ -847,6 +865,12 @@
847865
repoDir={branch.worktreePath}
848866
branchId={branch.id}
849867
projectId={branch.projectId}
868+
noteInfo={findNoteForSession(sessionMgr.openSessionId)}
869+
onOpenNote={(noteId, title, content) => {
870+
const sid = sessionMgr.openSessionId;
871+
sessionMgr.openSessionId = null;
872+
openNote = { title, content, sessionId: sid ?? undefined };
873+
}}
850874
onClose={async () => {
851875
const closedSessionId = sessionMgr.openSessionId;
852876
sessionMgr.openSessionId = null;

apps/staged/src/lib/features/notes/NoteModal.svelte

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
title: string;
2222
content: string;
2323
onClose: () => void;
24+
/** When set, shows a button to open the associated chat session. */
25+
sessionId?: string | null;
26+
onOpenSession?: (sessionId: string) => void;
2427
}
2528
26-
let { title, content, onClose }: Props = $props();
29+
let { title, content, onClose, sessionId, onOpenSession }: Props = $props();
2730
2831
let copied = $state(false);
2932
const backdropDismiss = createBackdropDismissHandlers({ onDismiss: () => onClose() });
@@ -163,7 +166,9 @@
163166
<!-- svelte-ignore a11y_no_static_element_interactions -->
164167
<div class="modal" role="presentation" onclick={(e) => e.stopPropagation()}>
165168
<header class="modal-header">
166-
<h2 class="modal-title">{title}</h2>
169+
<div class="header-content">
170+
<span class="header-title">{title}</span>
171+
</div>
167172
<InContentSearch
168173
visible={searchVisible}
169174
{matchCount}
@@ -175,7 +180,7 @@
175180
/>
176181
<div class="header-actions">
177182
<button
178-
class="share-btn"
183+
class="header-btn"
179184
class:copied
180185
onclick={handleShare}
181186
title={copied ? 'Copied!' : 'Copy note to clipboard'}
@@ -186,6 +191,15 @@
186191
<Copy size={16} />
187192
{/if}
188193
</button>
194+
{#if sessionId && onOpenSession}
195+
<button
196+
class="header-btn"
197+
onclick={() => onOpenSession?.(sessionId!)}
198+
title="Open chat session"
199+
>
200+
View chat
201+
</button>
202+
{/if}
189203
<button class="close-btn" onclick={onClose} title="Close (Esc)">
190204
<X size={16} />
191205
</button>
@@ -218,9 +232,9 @@
218232
.modal {
219233
display: flex;
220234
flex-direction: column;
221-
width: 640px;
222-
max-width: 90vw;
223-
max-height: 80vh;
235+
width: 700px;
236+
height: 80vh;
237+
max-height: 900px;
224238
background: var(--bg-chrome);
225239
border-radius: 12px;
226240
overflow: hidden;
@@ -232,21 +246,27 @@
232246
display: flex;
233247
align-items: center;
234248
justify-content: space-between;
235-
padding: 16px 20px;
249+
padding: 12px 16px;
236250
border-bottom: 1px solid var(--border-subtle);
237251
flex-shrink: 0;
238252
gap: 12px;
239253
}
240254
241-
.modal-title {
242-
margin: 0;
243-
font-size: var(--size-base);
255+
.header-content {
256+
display: flex;
257+
align-items: center;
258+
gap: 10px;
259+
min-width: 0;
260+
flex: 1;
261+
}
262+
263+
.header-title {
264+
font-size: var(--size-sm);
244265
font-weight: 600;
245266
color: var(--text-primary);
246267
overflow: hidden;
247268
text-overflow: ellipsis;
248269
white-space: nowrap;
249-
min-width: 0;
250270
}
251271
252272
.close-btn {
@@ -277,28 +297,31 @@
277297
flex-shrink: 0;
278298
}
279299
280-
.share-btn {
300+
.header-btn {
281301
display: flex;
282302
align-items: center;
283303
justify-content: center;
284-
padding: 4px;
304+
padding: 4px 10px;
285305
background: none;
286-
border: none;
306+
border: 1px solid var(--border-muted);
287307
border-radius: 6px;
288308
color: var(--text-muted);
289309
cursor: pointer;
290310
flex-shrink: 0;
311+
font-size: 12px;
291312
transition:
292313
color 0.1s,
293-
background-color 0.1s;
314+
background-color 0.1s,
315+
border-color 0.1s;
294316
}
295317
296-
.share-btn:hover {
318+
.header-btn:hover {
297319
color: var(--text-primary);
298320
background: var(--bg-hover);
321+
border-color: var(--text-muted);
299322
}
300323
301-
.share-btn.copied {
324+
.header-btn.copied {
302325
color: var(--status-added);
303326
}
304327

apps/staged/src/lib/features/projects/ProjectSection.svelte

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@
373373
})
374374
);
375375
376-
let openNote = $state<{ title: string; content: string } | null>(null);
376+
let openNote = $state<{ title: string; content: string; sessionId?: string } | null>(null);
377377
let openSessionId = $state<string | null>(null);
378378
379379
function formatRelativeTime(timestampMs: number): string {
@@ -614,7 +614,11 @@
614614
onItemClick={isRunning || isFailed
615615
? undefined
616616
: () => {
617-
openNote = { title: note.title, content: note.content };
617+
openNote = {
618+
title: note.title,
619+
content: note.content,
620+
sessionId: note.sessionId ?? undefined,
621+
};
618622
}}
619623
onSessionClick={(sid) => {
620624
openSessionId = sid;
@@ -646,13 +650,33 @@
646650
</div>
647651

648652
{#if openNote}
649-
<NoteModal title={openNote.title} content={openNote.content} onClose={() => (openNote = null)} />
653+
<NoteModal
654+
title={openNote.title}
655+
content={openNote.content}
656+
sessionId={openNote.sessionId}
657+
onClose={() => (openNote = null)}
658+
onOpenSession={(sid) => {
659+
openNote = null;
660+
openSessionId = sid;
661+
}}
662+
/>
650663
{/if}
651664

652665
{#if openSessionId}
666+
{@const noteForSession = projectNotes.find(
667+
(n) => n.sessionId === openSessionId && n.content?.trim()
668+
)}
653669
<SessionModal
654670
sessionId={openSessionId}
655671
projectId={project.id}
672+
noteInfo={noteForSession
673+
? { id: noteForSession.id, title: noteForSession.title, content: noteForSession.content }
674+
: null}
675+
onOpenNote={(_noteId, title, content) => {
676+
const sid = openSessionId;
677+
openSessionId = null;
678+
openNote = { title, content, sessionId: sid ?? undefined };
679+
}}
656680
onClose={() => {
657681
openSessionId = null;
658682
loadProjectNotes();

apps/staged/src/lib/features/sessions/SessionModal.svelte

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,12 @@
8383
branchId?: string | null;
8484
/** Project ID — when provided, enables image attachment on replies. */
8585
projectId?: string | null;
86+
/** When set, shows a button to open the associated note. */
87+
noteInfo?: { id: string; title: string; content: string } | null;
88+
onOpenNote?: (noteId: string, title: string, content: string) => void;
8689
}
8790
88-
let { sessionId, onClose, repoDir, branchId, projectId }: Props = $props();
91+
let { sessionId, onClose, repoDir, branchId, projectId, noteInfo, onOpenNote }: Props = $props();
8992
9093
// =========================================================================
9194
// State
@@ -820,6 +823,15 @@
820823
onClose={closeSearch}
821824
/>
822825
<div class="header-actions">
826+
{#if noteInfo && onOpenNote}
827+
<button
828+
class="header-btn"
829+
onclick={() => onOpenNote?.(noteInfo!.id, noteInfo!.title, noteInfo!.content)}
830+
title="Open note"
831+
>
832+
View note
833+
</button>
834+
{/if}
823835
<button class="close-btn" onclick={requestClose} title="Close (Esc)">
824836
<X size={16} />
825837
</button>
@@ -1249,6 +1261,30 @@
12491261
flex-shrink: 0;
12501262
}
12511263
1264+
.header-btn {
1265+
display: flex;
1266+
align-items: center;
1267+
justify-content: center;
1268+
padding: 4px 10px;
1269+
background: none;
1270+
border: 1px solid var(--border-muted);
1271+
border-radius: 6px;
1272+
color: var(--text-muted);
1273+
cursor: pointer;
1274+
flex-shrink: 0;
1275+
font-size: 12px;
1276+
transition:
1277+
color 0.1s,
1278+
background-color 0.1s,
1279+
border-color 0.1s;
1280+
}
1281+
1282+
.header-btn:hover {
1283+
color: var(--text-primary);
1284+
background: var(--bg-hover);
1285+
border-color: var(--text-muted);
1286+
}
1287+
12521288
.close-btn {
12531289
display: flex;
12541290
align-items: center;

apps/staged/src/lib/features/timeline/BranchTimeline.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
deletingItems?: { type: 'commit' | 'note' | 'review' | 'image'; id: string }[];
4343
onSessionClick?: (sessionId: string) => void;
4444
onCommitClick?: (sha: string) => void;
45-
onNoteClick?: (noteId: string, title: string, content: string) => void;
45+
onNoteClick?: (noteId: string, title: string, content: string, sessionId?: string) => void;
4646
onReviewClick?: (reviewId: string) => void;
4747
onImageClick?: (imageId: string) => void;
4848
onDeleteCommit?: (sha: string, sessionId?: string) => void;
@@ -351,7 +351,7 @@
351351
if (item.type === 'commit' && item.commitSha && onCommitClick) {
352352
onCommitClick(item.commitSha);
353353
} else if (item.type === 'note' && item.noteId && onNoteClick) {
354-
onNoteClick(item.noteId, item.noteTitle ?? '', item.noteContent ?? '');
354+
onNoteClick(item.noteId, item.noteTitle ?? '', item.noteContent ?? '', item.sessionId);
355355
} else if (item.type === 'review' && item.reviewId && onReviewClick) {
356356
onReviewClick(item.reviewId);
357357
} else if (item.type === 'image' && item.imageId && onImageClick) {

0 commit comments

Comments
 (0)