Skip to content

Commit 15afaa8

Browse files
joewinkeclaude
andcommitted
feature(jat-z3xxz): show everything from task detail drawer on mobile session drawer > detail
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 56830c8 commit 15afaa8

1 file changed

Lines changed: 156 additions & 1 deletion

File tree

ide/src/lib/components/work/MobileSessionDrawer.svelte

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
1919
import { onMount, onDestroy } from 'svelte';
20+
import { stripAnsi } from '$lib/utils/ansiToHtml';
2021
import { fly, fade } from 'svelte/transition';
2122
import { cubicOut } from 'svelte/easing';
2223
import { richPaste } from '$lib/actions/richPaste';
@@ -409,12 +410,29 @@
409410
let output = $state('');
410411
let pollInterval: ReturnType<typeof setInterval> | null = null;
411412
413+
const detectedResumeSessionId = $derived.by((): string | null => {
414+
if (!output) return null;
415+
const recentOutput = stripAnsi(output.slice(-2000));
416+
const match = recentOutput.match(
417+
/Resume this session with:\s*\nclaude --resume ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
418+
);
419+
return match ? match[1] : null;
420+
});
421+
412422
// Full task detail (fetched from API)
413423
let fullTask = $state<Record<string, any> | null>(null);
414424
let taskLoading = $state(false);
415425
let commentsCount = $state(0);
416426
let hasPendingQuestion = $state(false);
417427
428+
// Fetch full task when on Detail or Timeline — covers deep-link opens
429+
// where navigateToPage() is never called (initialPage = 'Detail')
430+
$effect(() => {
431+
if (logicalPage !== 'Terminal' && task?.id && !fullTask) {
432+
fetchTaskDetail();
433+
}
434+
});
435+
418436
// Dynamic page order: when agent has a pending question, Timeline moves to position 1
419437
const pageOrder = $derived.by((): LogicalPage[] =>
420438
hasPendingQuestion ? ['Terminal', 'Timeline', 'Detail'] : ['Terminal', 'Detail', 'Timeline']
@@ -1074,6 +1092,17 @@
10741092
}
10751093
10761094
let copiedAttachmentId = $state<string | null>(null);
1095+
let copiedPageUrl = $state(false);
1096+
1097+
function copyPageUrl() {
1098+
const url = fullTask?.page_url;
1099+
if (!url) return;
1100+
navigator.clipboard.writeText(url).then(() => {
1101+
copiedPageUrl = true;
1102+
setTimeout(() => (copiedPageUrl = false), 1500);
1103+
}).catch(() => {});
1104+
}
1105+
10771106
function copyAttachmentPath(attachment: any) {
10781107
const path = attachment?.path || attachment?.name || attachment?.filename;
10791108
if (!path) return;
@@ -1528,6 +1557,10 @@
15281557
}
15291558
loadHistory();
15301559
1560+
// Fetch output immediately on mount so state-detection derived values
1561+
// (like detectedResumeSessionId) are populated without waiting for resize.
1562+
fetchOutput();
1563+
15311564
// Resize tmux pane to match viewport width, then fetch output so the
15321565
// first render reflects the new column count (not the old narrow width).
15331566
// The 300ms delay gives Claude Code time to redraw after SIGWINCH.
@@ -1748,9 +1781,27 @@
17481781
</div>
17491782

17501783
<!-- Mobile Action Buttons Row (dynamic from state actions config) -->
1751-
{#if stateActions.length > 0}
1784+
{#if stateActions.length > 0 || detectedResumeSessionId}
17521785
<div class="action-pills-wrapper bg-base-200 border-t border-base-300 flex-shrink-0">
17531786
<div class="flex gap-1.5 px-2 py-1.5 overflow-x-auto">
1787+
{#if detectedResumeSessionId}
1788+
<button
1789+
use:directClick={async () => {
1790+
if (!onSendInput) return;
1791+
await onSendInput('ctrl-u', 'key');
1792+
await onSendInput(`claude --resume ${detectedResumeSessionId} --dangerously-skip-permissions`, 'text');
1793+
await onSendInput('enter', 'key');
1794+
}}
1795+
class="flex items-center gap-1 px-2 py-[0.3rem] text-[0.6875rem] font-medium rounded-md whitespace-nowrap cursor-pointer flex-shrink-0 border transition-colors active:brightness-125"
1796+
style="background: oklch(0.28 0.14 260 / 0.8); border-color: oklch(0.55 0.18 260 / 0.5); color: oklch(0.92 0.08 260);"
1797+
title={`Resume: claude --resume ${detectedResumeSessionId} --dangerously-skip-permissions`}
1798+
>
1799+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="14" height="14">
1800+
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
1801+
</svg>
1802+
<span>Resume</span>
1803+
</button>
1804+
{/if}
17541805
{#each stateActions as action (action.id)}
17551806
{@const isDestructive = DESTRUCTIVE_ACTIONS.has(action.id)}
17561807
<button
@@ -2048,6 +2099,86 @@
20482099
</div>
20492100
{/if}
20502101

2102+
<!-- Feedback Context (JST app feedback: page URL, recording, selected elements) -->
2103+
{#if fullTask?.page_url || fullTask?.recording_url || fullTask?.selected_elements?.length}
2104+
<div role="region" aria-label="Feedback Context" class="mb-4 rounded-lg overflow-hidden" style="border: 1px solid oklch(0.70 0.18 200 / 0.35); background: oklch(0.16 0.02 200);">
2105+
<div class="px-3 py-2.5 flex flex-col gap-2">
2106+
<div class="text-[10px] font-mono uppercase tracking-widest text-base-content/30">Feedback Context</div>
2107+
{#if fullTask.recording_url}
2108+
{@const replayBase = (() => { try { return new URL(fullTask.page_url || '').origin; } catch { return ''; } })()}
2109+
{@const replayUrl = fullTask.db_id && replayBase ? `${replayBase}/feedback/replay?id=${fullTask.db_id}` : fullTask.recording_url || ''}
2110+
{#if replayUrl}
2111+
<a
2112+
href={replayUrl}
2113+
target="_blank"
2114+
rel="noopener noreferrer"
2115+
class="feedback-recording-cta inline-flex items-center gap-2 self-start px-3 py-1.5 rounded-full text-xs font-medium"
2116+
>
2117+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
2118+
View Recording
2119+
</a>
2120+
{/if}
2121+
{/if}
2122+
{#if fullTask.page_url}
2123+
<div class="flex items-center gap-1 group/pageurl">
2124+
<a
2125+
href={fullTask.page_url}
2126+
target="_blank"
2127+
rel="noopener noreferrer"
2128+
class="feedback-page-url inline-flex items-center gap-1.5 flex-1 min-w-0 rounded px-1 py-0.5 -mx-1 -my-0.5 transition-colors"
2129+
aria-label="Open page: {fullTask.page_url}"
2130+
title={fullTask.page_url}
2131+
>
2132+
<svg class="w-3 h-3 flex-shrink-0 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
2133+
<span class="text-xs text-base-content/50 group-hover/pageurl:text-base-content/80 truncate transition-colors">{fullTask.page_url}</span>
2134+
</a>
2135+
<button
2136+
type="button"
2137+
class="opacity-0 group-hover/pageurl:opacity-100 transition-opacity flex-shrink-0 p-0.5 rounded hover:bg-base-300/50"
2138+
use:directClick={copyPageUrl}
2139+
title={copiedPageUrl ? 'Copied!' : 'Copy URL'}
2140+
aria-label="Copy page URL"
2141+
>
2142+
{#if copiedPageUrl}
2143+
<svg class="w-3 h-3 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
2144+
{:else}
2145+
<svg class="w-3 h-3 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"/></svg>
2146+
{/if}
2147+
</button>
2148+
</div>
2149+
{/if}
2150+
{#if fullTask.selected_elements?.length}
2151+
<div class="flex flex-col gap-1 mt-0.5">
2152+
{#each fullTask.selected_elements as el}
2153+
{#if el.tagName || el.textContent?.trim()}
2154+
<div class="rounded bg-base-300/40 px-2 py-1 text-xs flex items-center gap-2">
2155+
{#if el.tagName}
2156+
<span class="badge badge-xs badge-ghost font-mono uppercase flex-shrink-0">{el.tagName}</span>
2157+
{/if}
2158+
{#if el.textContent?.trim()}
2159+
<span class="text-base-content/50 truncate">"{el.textContent.trim().slice(0, 80)}"</span>
2160+
{/if}
2161+
</div>
2162+
{/if}
2163+
{/each}
2164+
</div>
2165+
{/if}
2166+
{#if fullTask.user_agent}
2167+
{@const ua = fullTask.user_agent}
2168+
{@const browserMatch = ua.match(/Chrome\/(\d+)|Firefox\/(\d+)|Safari\/(\d+)/)}
2169+
{@const osMatch = ua.match(/Mac OS X|Windows NT|Linux|Android|iPhone|iPad/)}
2170+
{@const browserStr = browserMatch
2171+
? (ua.includes('Chrome') ? `Chrome ${browserMatch[1]}` : ua.includes('Firefox') ? `Firefox ${browserMatch[2]}` : `Safari ${browserMatch[3]}`)
2172+
: ua.slice(0, 40)}
2173+
{@const osStr = osMatch
2174+
? (ua.includes('Mac OS X') ? 'macOS' : ua.includes('Windows') ? 'Windows' : ua.includes('Linux') ? 'Linux' : ua.includes('iPhone') ? 'iPhone' : ua.includes('iPad') ? 'iPad' : 'Android')
2175+
: ''}
2176+
<span class="text-xs font-mono text-base-content/30">{browserStr}{osStr ? ` / ${osStr}` : ''}</span>
2177+
{/if}
2178+
</div>
2179+
</div>
2180+
{/if}
2181+
20512182
<div
20522183
role="button"
20532184
tabindex="0"
@@ -2519,6 +2650,30 @@
25192650
</div>
25202651

25212652
<style>
2653+
/* Feedback Context: recording CTA button */
2654+
.feedback-recording-cta {
2655+
background: oklch(0.70 0.18 200 / 0.15);
2656+
color: oklch(0.75 0.18 200);
2657+
border: 1px solid oklch(0.70 0.18 200 / 0.30);
2658+
transition: background 150ms ease;
2659+
}
2660+
.feedback-recording-cta:hover {
2661+
background: oklch(0.70 0.18 200 / 0.25);
2662+
}
2663+
.feedback-recording-cta:focus-visible {
2664+
outline: 2px solid oklch(0.70 0.18 200 / 0.70);
2665+
outline-offset: 2px;
2666+
}
2667+
2668+
/* Feedback Context: page URL link */
2669+
.feedback-page-url:hover {
2670+
background: oklch(0.25 0.01 250 / 0.40);
2671+
}
2672+
.feedback-page-url:focus-visible {
2673+
outline: 2px solid oklch(0.70 0.18 200 / 0.70);
2674+
outline-offset: 1px;
2675+
}
2676+
25222677
.mic-recording {
25232678
animation: mic-pulse 1.4s ease-in-out infinite;
25242679
}

0 commit comments

Comments
 (0)