11<script setup lang="ts">
22import { ref , computed , onMounted , watch } from ' vue'
3+ import { RouterLink } from ' vue-router'
34import { logServiceRpcCall } from ' @/composables/useRpc'
45import { useAutoRefresh } from ' @/composables/useAutoRefresh'
56import type { FetchLogsResponse , LogEntry , TaskAttempt } from ' @/types/rpc'
@@ -159,7 +160,40 @@ watch(() => props.workerId, resetAndFetch)
159160
160161onMounted (resetAndFetch )
161162
162- const filteredLogs = computed <LogEntry []>(() => entries .value )
163+ // Job-aggregate mode shows logs from many tasks; render a per-line link to the
164+ // originating task. Single-task mode would link every line to itself, so skip.
165+ const showTaskLinks = computed (() => {
166+ if (! props .taskId ) return false
167+ return ! / \/ \d + $ / .test (props .taskId )
168+ })
169+
170+ interface TaskRef {
171+ taskId: string
172+ taskIndex: string
173+ }
174+
175+ function parseTaskFromKey(key : string | undefined ): TaskRef | null {
176+ if (! key ) return null
177+ const colonIdx = key .lastIndexOf (' :' )
178+ const taskId = colonIdx > 0 ? key .slice (0 , colonIdx ) : key
179+ const lastSlash = taskId .lastIndexOf (' /' )
180+ if (lastSlash < 0 ) return null
181+ const taskIndex = taskId .slice (lastSlash + 1 )
182+ if (! / ^ \d + $ / .test (taskIndex )) return null
183+ return { taskId , taskIndex }
184+ }
185+
186+ interface LogRow {
187+ entry: LogEntry
188+ taskRef: TaskRef | null
189+ }
190+
191+ const logRows = computed <LogRow []>(() =>
192+ entries .value .map (entry => ({
193+ entry ,
194+ taskRef: showTaskLinks .value ? parseTaskFromKey (entry .key ) : null ,
195+ })),
196+ )
163197
164198defineExpose ({ selectedAttemptId })
165199 </script >
@@ -211,7 +245,7 @@ defineExpose({ selectedAttemptId })
211245 {{ autoRefreshActive ? 'Auto ⟳' : 'Paused' }}
212246 </button >
213247 <span class =" ml-auto text-xs text-text-muted font-mono" >
214- {{ filteredLogs .length }} lines
248+ {{ logRows .length }} lines
215249 </span >
216250 </div >
217251
@@ -227,27 +261,35 @@ defineExpose({ selectedAttemptId })
227261 :style =" { maxHeight: maxHeight }"
228262 >
229263 <div
230- v-if =" loading && filteredLogs .length === 0"
264+ v-if =" loading && logRows .length === 0"
231265 class =" py-12 text-center text-text-muted text-sm"
232266 >
233267 Loading logs...
234268 </div >
235269 <div
236- v-else-if =" filteredLogs .length === 0"
270+ v-else-if =" logRows .length === 0"
237271 class =" py-12 text-center text-text-muted text-sm"
238272 >
239273 No log entries
240274 </div >
241275 <div
242- v-for =" (entry , i) in filteredLogs "
276+ v-for =" (row , i) in logRows "
243277 :key =" i"
244278 :class =" [
245279 'px-3 py-0.5 font-mono text-xs leading-relaxed hover:bg-surface-sunken',
246- logLevelClass(entry.level),
280+ logLevelClass(row. entry.level),
247281 ]"
248282 >
249- <span class =" text-text-muted mr-2" >{{ formatLogTime(timestampMs(entry.timestamp)) }}</span >
250- <span class =" whitespace-pre-wrap break-all" >{{ entry.data }}</span >
283+ <RouterLink
284+ v-if =" row.taskRef && props.taskId"
285+ :to =" `/job/${encodeURIComponent(props.taskId)}/task/${encodeURIComponent(row.taskRef.taskId)}`"
286+ class =" text-accent hover:underline mr-2"
287+ :title =" row.taskRef.taskId"
288+ >
289+ T{{ row.taskRef.taskIndex }}
290+ </RouterLink >
291+ <span class =" text-text-muted mr-2" >{{ formatLogTime(timestampMs(row.entry.timestamp)) }}</span >
292+ <span class =" whitespace-pre-wrap break-all" >{{ row.entry.data }}</span >
251293 </div >
252294 </div >
253295 </div >
0 commit comments