diff --git a/bun.lock b/bun.lock
index 0ed85e8c3..6dd484e38 100644
--- a/bun.lock
+++ b/bun.lock
@@ -882,6 +882,8 @@
"@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.15.2", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-wwQM88HATws1quf94M6D4rbkWXNVfIcAMA2WKae+i8/be2vvAi8uuvfV7HHqsqeozUmMJ3NEC6zKYhEtlmMuoA=="],
+ "@twsxtd/hapi-win32-x64": ["@twsxtd/hapi-win32-x64@0.15.2", "", { "os": "win32", "cpu": "x64", "bin": { "hapi": "bin/hapi.exe" } }, "sha512-vuQH9INKvqwVZ9JphUmEyzKz6zWxHy3GE4j0mDqTulO234nGEhljkN3Qod0d7bLVncmO53zSMcm+ahwZ3qL4OA=="],
+
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
diff --git a/shared/src/sessionSummary.ts b/shared/src/sessionSummary.ts
index 4b693ada7..fd5096dc6 100644
--- a/shared/src/sessionSummary.ts
+++ b/shared/src/sessionSummary.ts
@@ -1,4 +1,4 @@
-import type { ModelMode } from './modes'
+import type { ModelMode, PermissionMode } from './modes'
import type { Session, WorktreeMetadata } from './schemas'
export type SessionSummaryMetadata = {
@@ -19,6 +19,7 @@ export type SessionSummary = {
metadata: SessionSummaryMetadata | null
todoProgress: { completed: number; total: number } | null
pendingRequestsCount: number
+ permissionMode?: PermissionMode
modelMode?: ModelMode
}
@@ -48,6 +49,7 @@ export function toSessionSummary(session: Session): SessionSummary {
metadata,
todoProgress,
pendingRequestsCount,
+ permissionMode: session.permissionMode,
modelMode: session.modelMode
}
}
diff --git a/web/src/components/SessionHeader.tsx b/web/src/components/SessionHeader.tsx
index 3728b2670..4fbb24828 100644
--- a/web/src/components/SessionHeader.tsx
+++ b/web/src/components/SessionHeader.tsx
@@ -1,12 +1,14 @@
import { useId, useMemo, useRef, useState } from 'react'
import type { Session } from '@/types/api'
import type { ApiClient } from '@/api/client'
+import { getPermissionModeLabel, getPermissionModeTone, isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import { isTelegramApp } from '@/hooks/useTelegram'
import { useSessionActions } from '@/hooks/mutations/useSessionActions'
import { SessionActionMenu } from '@/components/SessionActionMenu'
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
import { useTranslation } from '@/lib/use-translation'
+import { getFlavorBadgeClass, PERMISSION_TONE_BADGE } from '@/lib/agentFlavorUtils'
function getSessionTitle(session: Session): string {
if (session.metadata?.name) {
@@ -128,21 +130,52 @@ export function SessionHeader(props: {
- {/* Session info - two lines: title and path */}
+ {/* Session info - two lines: title and badges */}
{title}
-
-
- ❖
- {session.metadata?.flavor?.trim() || 'unknown'}
-
-
- {t('session.item.modelMode')}: {session.modelMode || 'default'}
-
+
+ {(() => {
+ const flavor = session.metadata?.flavor?.trim() ?? null
+ const flavorBadge = getFlavorBadgeClass(flavor)
+ const label = flavor || 'unknown'
+ return (
+
+ {label}
+
+ )
+ })()}
+ {(() => {
+ const flavor = session.metadata?.flavor?.trim() ?? null
+ const permMode = session.permissionMode
+ && session.permissionMode !== 'default'
+ && isPermissionModeAllowedForFlavor(session.permissionMode, flavor)
+ ? session.permissionMode
+ : null
+ if (!permMode) return null
+ const label = getPermissionModeLabel(permMode).toLowerCase()
+ const tone = getPermissionModeTone(permMode)
+ const badgeClass = PERMISSION_TONE_BADGE[tone]
+ return (
+
+ {label}
+
+ )
+ })()}
+ {(() => {
+ const flav = session.metadata?.flavor?.trim() ?? null
+ if (flav && flav !== 'claude') return null
+ return (
+
+ {session.modelMode || 'default'}
+
+ )
+ })()}
{worktreeBranch ? (
- {t('session.item.worktree')}: {worktreeBranch}
+
+ {worktreeBranch}
+
) : null}
diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx
index 69c71c37b..115a34f75 100644
--- a/web/src/components/SessionList.tsx
+++ b/web/src/components/SessionList.tsx
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import type { SessionSummary } from '@/types/api'
import type { ApiClient } from '@/api/client'
+import { getPermissionModeLabel, getPermissionModeTone, isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import { useLongPress } from '@/hooks/useLongPress'
import { usePlatform } from '@/hooks/usePlatform'
import { useSessionActions } from '@/hooks/mutations/useSessionActions'
@@ -8,10 +9,13 @@ import { SessionActionMenu } from '@/components/SessionActionMenu'
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
import { useTranslation } from '@/lib/use-translation'
+import { getFlavorTextClass, PERMISSION_TONE_TEXT } from '@/lib/agentFlavorUtils'
type SessionGroup = {
+ key: string
directory: string
displayName: string
+ machineId: string | null
sessions: SessionSummary[]
latestUpdatedAt: number
hasActiveSession: boolean
@@ -26,32 +30,46 @@ function getGroupDisplayName(directory: string): string {
}
function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] {
- const groups = new Map
()
+ const groups = new Map()
sessions.forEach(session => {
const path = session.metadata?.worktree?.basePath ?? session.metadata?.path ?? 'Other'
- if (!groups.has(path)) {
- groups.set(path, [])
+ const machineId = session.metadata?.machineId ?? null
+ const key = `${machineId ?? '__unknown__'}::${path}`
+ if (!groups.has(key)) {
+ groups.set(key, {
+ directory: path,
+ machineId,
+ sessions: []
+ })
}
- groups.get(path)!.push(session)
+ groups.get(key)!.sessions.push(session)
})
return Array.from(groups.entries())
- .map(([directory, groupSessions]) => {
- const sortedSessions = [...groupSessions].sort((a, b) => {
+ .map(([key, group]) => {
+ const sortedSessions = [...group.sessions].sort((a, b) => {
const rankA = a.active ? (a.pendingRequestsCount > 0 ? 0 : 1) : 2
const rankB = b.active ? (b.pendingRequestsCount > 0 ? 0 : 1) : 2
if (rankA !== rankB) return rankA - rankB
return b.updatedAt - a.updatedAt
})
- const latestUpdatedAt = groupSessions.reduce(
+ const latestUpdatedAt = group.sessions.reduce(
(max, s) => (s.updatedAt > max ? s.updatedAt : max),
-Infinity
)
- const hasActiveSession = groupSessions.some(s => s.active)
- const displayName = getGroupDisplayName(directory)
+ const hasActiveSession = group.sessions.some(s => s.active)
+ const displayName = getGroupDisplayName(group.directory)
- return { directory, displayName, sessions: sortedSessions, latestUpdatedAt, hasActiveSession }
+ return {
+ key,
+ directory: group.directory,
+ displayName,
+ machineId: group.machineId,
+ sessions: sortedSessions,
+ latestUpdatedAt,
+ hasActiveSession
+ }
})
.sort((a, b) => {
if (a.hasActiveSession !== b.hasActiveSession) {
@@ -147,6 +165,27 @@ function getAgentLabel(session: SessionSummary): string {
return 'unknown'
}
+function MachineIcon(props: { className?: string }) {
+ return (
+
+ )
+}
+
function formatRelativeTime(value: number, t: (key: string, params?: Record) => string): string | null {
const ms = value < 1_000_000_000_000 ? value * 1000 : value
if (!Number.isFinite(ms)) return null
@@ -201,20 +240,33 @@ function SessionItem(props: {
const statusDotClass = s.active
? (s.thinking ? 'bg-[#007AFF]' : 'bg-[var(--app-badge-success-text)]')
: 'bg-[var(--app-hint)]'
+
+ const flavor = s.metadata?.flavor?.trim() ?? null
+ const flavorTextClass = getFlavorTextClass(flavor)
+
+ const permMode = s.permissionMode
+ && s.permissionMode !== 'default'
+ && isPermissionModeAllowedForFlavor(s.permissionMode, flavor)
+ ? s.permissionMode
+ : null
+ const permLabel = permMode ? getPermissionModeLabel(permMode).toLowerCase() : null
+ const permTone = permMode ? getPermissionModeTone(permMode) : null
+ const permTextClass = permTone ? PERMISSION_TONE_TEXT[permTone] : ''
+
return (
<>