Skip to content

Commit 4760bd7

Browse files
joewinkeclaude
andcommitted
feature(jat-rlsm0): per-action visual feedback on mobilenew hover tray
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a4867e7 commit 4760bd7

File tree

10 files changed

+193
-6765
lines changed

10 files changed

+193
-6765
lines changed

ide/src/lib/components/Sidebar.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
function getGroupItems(groupId: NavGroup) {
4949
const debugMode = getDebugMode();
5050
return unifiedNavConfig.navItems.filter((item) => {
51-
if (!debugMode && (item.id === 'mobile' || item.id === 'mobilenew' || item.id === 'open-tasks' || item.id === 'clients')) return false;
51+
if (!debugMode && (item.id === 'open-tasks' || item.id === 'clients')) return false;
5252
return item.category === groupId;
5353
});
5454
}

ide/src/lib/components/sessions/TasksActive.svelte

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@
235235
// Action loading state
236236
let actionLoading = $state<string | null>(null);
237237
238+
// Per-action visual feedback — tracks which button was just clicked per session
239+
// key: "sessionName:actionId", value: feedback variant ('success' | 'warning' | 'error' | 'info' | 'working')
240+
let actionFeedback = $state<Map<string, string>>(new Map());
241+
238242
// Per-session auto-complete disabled state (when user manually overrides)
239243
let autoCompleteDisabledMap = $state<Map<string, boolean>>(new Map());
240244
@@ -904,14 +908,32 @@
904908
let swipeState = $state<SwipeState | null>(null);
905909
let swipeOffsets = $state<Map<string, number>>(new Map());
906910
911+
// Set visual feedback on a tray button, auto-clears after animation
912+
function setActionFeedback(sessionName: string, actionId: string, variant: string, durationMs = 800) {
913+
const key = `${sessionName}:${actionId}`;
914+
actionFeedback.set(key, variant);
915+
actionFeedback = new Map(actionFeedback);
916+
setTimeout(() => {
917+
actionFeedback.delete(key);
918+
actionFeedback = new Map(actionFeedback);
919+
}, durationMs);
920+
}
921+
907922
async function handleMobileAction(actionId: string, sessionName: string, sessionTask: AgentTask | null, agentName: string, project: string | null) {
923+
// Prevent double-clicks while feedback is active
924+
const feedbackKey = `${sessionName}:${actionId}`;
925+
if (actionFeedback.has(feedbackKey)) return;
926+
908927
if (actionId === 'attach') {
928+
setActionFeedback(sessionName, actionId, 'info');
909929
await handleAttachSession(sessionName);
910930
} else if (actionId === 'kill' || actionId === 'cleanup') {
931+
setActionFeedback(sessionName, actionId, 'error', 1200);
911932
await handleKillSession(sessionName);
912933
} else if (actionId === 'view-task' && sessionTask) {
913934
onViewTask?.(sessionTask.id);
914935
} else if (actionId === 'complete' || actionId === 'complete-kill') {
936+
setActionFeedback(sessionName, actionId, 'success', 1500);
915937
optimisticStates.set(sessionName, 'completing');
916938
optimisticStates = new Map(optimisticStates);
917939
if (sessionTask) {
@@ -924,21 +946,26 @@
924946
}
925947
await sendWorkflowCommand(sessionName, actionId === 'complete-kill' ? '/jat:complete --kill' : '/jat:complete');
926948
} else if (actionId === 'interrupt') {
949+
setActionFeedback(sessionName, actionId, 'warning');
927950
await fetch(`/api/work/${encodeURIComponent(sessionName)}/input`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'ctrl-c' }) });
928951
} else if (actionId === 'escape') {
952+
setActionFeedback(sessionName, actionId, 'warning');
929953
await fetch(`/api/work/${encodeURIComponent(sessionName)}/input`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'escape' }) });
930954
} else if (actionId === 'close-kill') {
955+
setActionFeedback(sessionName, actionId, 'error', 1200);
931956
if (sessionTask) {
932957
try { await fetch(`/api/tasks/${encodeURIComponent(sessionTask.id)}/close`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Abandoned via Close & Kill' }) }); } catch (e) { console.warn('[TasksActive] close task failed:', e); }
933958
}
934959
await handleKillSession(sessionName);
935960
} else if (actionId === 'pause') {
961+
setActionFeedback(sessionName, actionId, 'info', 1200);
936962
optimisticStates.set(sessionName, 'paused');
937963
optimisticStates = new Map(optimisticStates);
938964
if (sessionTask) {
939965
try { await fetch(`/api/sessions/${encodeURIComponent(sessionName)}/pause`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: sessionTask.id, taskTitle: sessionTask.title, reason: 'Paused via mobile tray', killSession: true, agentName, project }) }); } catch (e) { console.warn('[TasksActive] pause failed:', e); }
940966
} else { await handleKillSession(sessionName); }
941967
} else if (actionId === 'convert-to-tasks') {
968+
setActionFeedback(sessionName, actionId, 'info');
942969
await sendWorkflowCommand(sessionName, '/jat:tasktree');
943970
}
944971
}
@@ -1328,9 +1355,14 @@
13281355
</div>
13291356
<div class="mobile-action-tray" role="group" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
13301357
{#each cardActions.slice(0, 4) as action}
1331-
<button class="mobile-tray-btn mobile-tray-btn-{action.variant}" title={action.description} onclick={() => handleMobileAction(action.id, session.name, null, sessionAgentName, session.project || null)}>
1332-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1333-
<span>{action.label}</span>
1358+
{@const fb = actionFeedback.get(`${session.name}:${action.id}`)}
1359+
<button class="mobile-tray-btn mobile-tray-btn-{fb ? fb : action.variant}" class:mobile-tray-btn-feedback={!!fb} title={action.description} disabled={!!fb} onclick={() => handleMobileAction(action.id, session.name, null, sessionAgentName, session.project || null)}>
1360+
{#if fb}
1361+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
1362+
{:else}
1363+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1364+
{/if}
1365+
<span>{fb ? 'Done' : action.label}</span>
13341366
</button>
13351367
{/each}
13361368
</div>
@@ -1345,14 +1377,19 @@
13451377
{@const harness = getTaskHarness(sessionTask)}
13461378
{@const cardActions = getSessionStateActions(effectiveState)}
13471379
<div class="mobile-card-inner">
1348-
<div class="mobile-state-strip" style="background: {stateVisual.bgTint}; border-right: 2px solid {stateVisual.accent};" aria-hidden="true">
1349-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" width="13" height="13" style="stroke: {stateVisual.accent};"><path stroke-linecap="round" stroke-linejoin="round" d={stateVisual.icon} /></svg>
1380+
<div class="mobile-state-strip mobile-state-strip-agent" style="background: {stateVisual.bgTint}; border-right: 2px solid {stateVisual.accent};">
1381+
<AgentAvatar name={sessionAgentName} size={22} showRing={true} sessionState={effectiveState} />
13501382
</div>
13511383
<div class="mobile-action-tray" role="group" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
13521384
{#each cardActions.slice(0, 4) as action}
1353-
<button class="mobile-tray-btn mobile-tray-btn-{action.variant}" title={action.description} onclick={() => handleMobileAction(action.id, session.name, sessionTask, sessionAgentName, session.project || null)}>
1354-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1355-
<span>{action.label}</span>
1385+
{@const fb = actionFeedback.get(`${session.name}:${action.id}`)}
1386+
<button class="mobile-tray-btn mobile-tray-btn-{fb ? fb : action.variant}" class:mobile-tray-btn-feedback={!!fb} title={action.description} disabled={!!fb} onclick={() => handleMobileAction(action.id, session.name, sessionTask, sessionAgentName, session.project || null)}>
1387+
{#if fb}
1388+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
1389+
{:else}
1390+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1391+
{/if}
1392+
<span>{fb ? 'Done' : action.label}</span>
13561393
</button>
13571394
{/each}
13581395
</div>
@@ -1369,8 +1406,6 @@
13691406
<span class="mobile-separator">·</span>
13701407
<span class="mobile-elapsed">{#if elapsed.showHours}{elapsed.hours}:{/if}{elapsed.minutes}:{elapsed.seconds}</span>
13711408
{/if}
1372-
<span class="mobile-separator">·</span>
1373-
<AgentAvatar name={sessionAgentName} size={16} showRing={true} sessionState={effectiveState} />
13741409
<span class="mobile-agent-name">{sessionAgentName}</span>
13751410
{#if sessionTask.issue_type}
13761411
<span class="mobile-separator">·</span>
@@ -1399,14 +1434,19 @@
13991434
<!-- Planning / no-task session -->
14001435
{@const cardActions = getSessionStateActions(effectiveState)}
14011436
<div class="mobile-card-inner">
1402-
<div class="mobile-state-strip" style="background: {stateVisual.bgTint}; border-right: 2px solid {stateVisual.accent};" aria-hidden="true">
1403-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" width="13" height="13" style="stroke: {stateVisual.accent};"><path stroke-linecap="round" stroke-linejoin="round" d={stateVisual.icon} /></svg>
1437+
<div class="mobile-state-strip mobile-state-strip-agent" style="background: {stateVisual.bgTint}; border-right: 2px solid {stateVisual.accent};">
1438+
<AgentAvatar name={sessionAgentName} size={22} showRing={true} sessionState={effectiveState} />
14041439
</div>
14051440
<div class="mobile-action-tray" role="group" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
14061441
{#each cardActions.slice(0, 4) as action}
1407-
<button class="mobile-tray-btn mobile-tray-btn-{action.variant}" title={action.description} onclick={() => handleMobileAction(action.id, session.name, null, sessionAgentName, session.project || null)}>
1408-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1409-
<span>{action.label}</span>
1442+
{@const fb = actionFeedback.get(`${session.name}:${action.id}`)}
1443+
<button class="mobile-tray-btn mobile-tray-btn-{fb ? fb : action.variant}" class:mobile-tray-btn-feedback={!!fb} title={action.description} disabled={!!fb} onclick={() => handleMobileAction(action.id, session.name, null, sessionAgentName, session.project || null)}>
1444+
{#if fb}
1445+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
1446+
{:else}
1447+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14"><path stroke-linecap="round" stroke-linejoin="round" d={action.icon} /></svg>
1448+
{/if}
1449+
<span>{fb ? 'Done' : action.label}</span>
14101450
</button>
14111451
{/each}
14121452
</div>
@@ -3266,9 +3306,14 @@
32663306
transition: filter 0.2s;
32673307
}
32683308
3309+
/* Agent variant: wider to fit avatar comfortably */
3310+
.mobile-state-strip-agent {
3311+
width: 40px;
3312+
}
3313+
32693314
/* Brighten strip on card hover */
32703315
.mobile-session-card:hover .mobile-state-strip {
3271-
filter: brightness(1.8) saturate(1.3);
3316+
filter: brightness(1.15) saturate(1.2);
32723317
}
32733318
32743319
/* Subtle row highlight when tray is active */
@@ -3324,6 +3369,45 @@
33243369
.mobile-tray-btn-error { background: oklch(0.45 0.16 25); }
33253370
.mobile-tray-btn-info { background: oklch(0.48 0.14 220); }
33263371
.mobile-tray-btn-default { background: oklch(0.30 0.02 250); }
3372+
.mobile-tray-btn-working { background: oklch(0.52 0.14 70); }
3373+
3374+
/* Feedback flash animation — plays when a tray button is clicked */
3375+
.mobile-tray-btn-feedback {
3376+
animation: tray-btn-confirm 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
3377+
pointer-events: none;
3378+
}
3379+
3380+
.mobile-tray-btn-feedback.mobile-tray-btn-success {
3381+
background: oklch(0.58 0.20 145);
3382+
}
3383+
.mobile-tray-btn-feedback.mobile-tray-btn-error {
3384+
background: oklch(0.55 0.20 25);
3385+
}
3386+
.mobile-tray-btn-feedback.mobile-tray-btn-warning {
3387+
background: oklch(0.60 0.16 70);
3388+
}
3389+
.mobile-tray-btn-feedback.mobile-tray-btn-info {
3390+
background: oklch(0.56 0.16 220);
3391+
}
3392+
3393+
@keyframes tray-btn-confirm {
3394+
0% {
3395+
transform: scale(1);
3396+
filter: brightness(1);
3397+
}
3398+
30% {
3399+
transform: scale(1.12);
3400+
filter: brightness(1.4);
3401+
}
3402+
60% {
3403+
transform: scale(0.97);
3404+
filter: brightness(1.2);
3405+
}
3406+
100% {
3407+
transform: scale(1);
3408+
filter: brightness(1.15);
3409+
}
3410+
}
33273411
33283412
/* Content area (full width minus strip) */
33293413
.mobile-card-body {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* - Left-to-right on page 0: dismiss drawer
1313
* - Left-to-right on page 1+: previous page
1414
*
15-
* Used by /mobilenew and /tasks routes for mobile session viewing.
15+
* Used by /tasks route for mobile session viewing.
1616
*/
1717
1818
import { onMount, onDestroy } from 'svelte';

ide/src/lib/config/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Default route for the IDE. Unknown routes redirect here.
1414
* Change this single value to update the default landing page everywhere.
1515
*/
16-
export const DEFAULT_ROUTE = '/mobile';
16+
export const DEFAULT_ROUTE = '/tasks';
1717

1818
// =============================================================================
1919
// TIMEOUTS (in milliseconds)

ide/src/lib/config/navConfig.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
* NAVIGATION STRUCTURE (collapsible groups):
66
*
77
* WORK (daily workflow):
8-
* /mobile - Mobile (mobile-optimized task view)
9-
* /tasks - Tasks (active sessions + open tasks with spawn)
8+
* /tasks - Tasks (active sessions + open tasks with spawn, mobile drawer support)
109
* /sessions - Sessions (all tmux sessions: agents, servers, other)
1110
* /history - History (completed task history with streak calendar)
1211
*
@@ -85,20 +84,6 @@ export interface UnifiedNavConfig {
8584
export const unifiedNavConfig: UnifiedNavConfig = {
8685
navItems: [
8786
// WORK: Daily workflow
88-
{
89-
id: 'mobile',
90-
label: 'Mobile',
91-
href: '/mobile',
92-
icon: 'mobile',
93-
category: 'work'
94-
},
95-
{
96-
id: 'mobilenew',
97-
label: 'Mobile New',
98-
href: '/mobilenew',
99-
icon: 'mobile',
100-
category: 'work'
101-
},
10287
{
10388
id: 'tasks',
10489
label: 'Tasks',

ide/src/lib/config/statusColors.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,13 @@ export const SESSION_STATE_ACTIONS: Record<string, SessionStateAction[]> = {
893893
variant: 'success',
894894
description: 'Complete task and review completion block'
895895
},
896+
{
897+
id: 'attach',
898+
label: 'Attach Terminal',
899+
icon: 'M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z',
900+
variant: 'info',
901+
description: 'Open session in terminal to review'
902+
},
896903
{
897904
id: 'complete-kill',
898905
label: 'Complete & Kill',
@@ -914,13 +921,6 @@ export const SESSION_STATE_ACTIONS: Record<string, SessionStateAction[]> = {
914921
variant: 'default',
915922
description: 'Save progress and close (resumable later)'
916923
},
917-
{
918-
id: 'attach',
919-
label: 'Attach Terminal',
920-
icon: 'M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z',
921-
variant: 'info',
922-
description: 'Open session in terminal to review'
923-
},
924924
{
925925
id: 'kill',
926926
label: 'Kill Session',
@@ -984,18 +984,18 @@ export const SESSION_STATE_ACTIONS: Record<string, SessionStateAction[]> = {
984984
],
985985
working: [
986986
{
987-
id: 'complete-kill',
988-
label: 'Complete & Kill',
989-
icon: 'M9 12.75L11.25 15 15 9.75m0 0l3 3m-3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
990-
variant: 'warning',
991-
description: 'Complete task and self-destruct session'
987+
id: 'attach',
988+
label: 'Attach Terminal',
989+
icon: 'M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z',
990+
variant: 'info',
991+
description: 'Open session in terminal'
992992
},
993993
{
994-
id: 'close-kill',
995-
label: 'Close & Kill',
996-
icon: 'M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
997-
variant: 'error',
998-
description: 'Close task immediately and kill session (skip completion)'
994+
id: 'interrupt',
995+
label: 'Interrupt',
996+
icon: 'M15.75 5.25v13.5m-7.5-13.5v13.5',
997+
variant: 'warning',
998+
description: 'Send Ctrl+C to interrupt'
999999
},
10001000
{
10011001
id: 'pause',
@@ -1005,18 +1005,18 @@ export const SESSION_STATE_ACTIONS: Record<string, SessionStateAction[]> = {
10051005
description: 'Save progress and close (resumable later)'
10061006
},
10071007
{
1008-
id: 'interrupt',
1009-
label: 'Interrupt',
1010-
icon: 'M15.75 5.25v13.5m-7.5-13.5v13.5',
1008+
id: 'complete-kill',
1009+
label: 'Complete & Kill',
1010+
icon: 'M9 12.75L11.25 15 15 9.75m0 0l3 3m-3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
10111011
variant: 'warning',
1012-
description: 'Send Ctrl+C to interrupt'
1012+
description: 'Complete task and self-destruct session'
10131013
},
10141014
{
1015-
id: 'attach',
1016-
label: 'Attach Terminal',
1017-
icon: 'M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z',
1018-
variant: 'info',
1019-
description: 'Open session in terminal'
1015+
id: 'close-kill',
1016+
label: 'Close & Kill',
1017+
icon: 'M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
1018+
variant: 'error',
1019+
description: 'Close task immediately and kill session (skip completion)'
10201020
},
10211021
{
10221022
id: 'kill',

0 commit comments

Comments
 (0)