@codori/client 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,10 +16,13 @@ import { useProjects } from '../composables/useProjects'
16
16
  import { useRpc } from '../composables/useRpc'
17
17
  import { useChatSubmitGuard } from '../composables/useChatSubmitGuard'
18
18
  import {
19
+ hideThinkingPlaceholder,
19
20
  ITEM_PART,
20
21
  eventToMessage,
21
22
  isSubagentActiveStatus,
22
23
  itemToMessages,
24
+ replaceStreamingMessage,
25
+ showThinkingPlaceholder,
23
26
  threadToMessages,
24
27
  upsertStreamingMessage,
25
28
  type ChatMessage,
@@ -54,6 +57,7 @@ import {
54
57
  normalizeThreadTokenUsage,
55
58
  resolveContextWindowState,
56
59
  resolveEffortOptions,
60
+ shouldShowContextWindowIndicator,
57
61
  visibleModelOptions,
58
62
  type ReasoningEffort
59
63
  } from '~~/shared/chat-prompt-controls'
@@ -171,7 +175,6 @@ const starterPrompts = computed(() => {
171
175
  ]
172
176
  })
173
177
 
174
- const hasKnownThreadUsage = computed(() => !activeThreadId.value || tokenUsage.value !== null)
175
178
  const effectiveModelList = computed(() => {
176
179
  const withSelected = ensureModelOption(
177
180
  availableModels.value.length > 0 ? availableModels.value : FALLBACK_MODELS,
@@ -199,8 +202,9 @@ const effortSelectItems = computed(() =>
199
202
  }))
200
203
  )
201
204
  const contextWindowState = computed(() =>
202
- resolveContextWindowState(tokenUsage.value, modelContextWindow.value, hasKnownThreadUsage.value)
205
+ resolveContextWindowState(tokenUsage.value, modelContextWindow.value)
203
206
  )
207
+ const showContextIndicator = computed(() => shouldShowContextWindowIndicator(contextWindowState.value))
204
208
  const contextUsedPercent = computed(() => contextWindowState.value.usedPercent ?? 0)
205
209
  const contextIndicatorLabel = computed(() => {
206
210
  const remainingPercent = contextWindowState.value.remainingPercent
@@ -505,6 +509,20 @@ const removeOptimisticMessage = (messageId: string) => {
505
509
  optimisticAttachmentSnapshots.delete(messageId)
506
510
  }
507
511
 
512
+ const clearThinkingPlaceholder = () => {
513
+ messages.value = hideThinkingPlaceholder(messages.value)
514
+ }
515
+
516
+ const ensureThinkingPlaceholder = () => {
517
+ messages.value = showThinkingPlaceholder(messages.value)
518
+ }
519
+
520
+ const clearThinkingPlaceholderForVisibleItem = (item: CodexThreadItem) => {
521
+ if (item.type !== 'userMessage') {
522
+ clearThinkingPlaceholder()
523
+ }
524
+ }
525
+
508
526
  const restoreDraftIfPristine = (text: string, submittedAttachments: DraftAttachment[]) => {
509
527
  if (!input.value.trim()) {
510
528
  input.value = text
@@ -1231,7 +1249,7 @@ const applySubagentNotification = (threadId: string, notification: CodexRpcNotif
1231
1249
  }
1232
1250
  for (const nextMessage of itemToMessages(params.item)) {
1233
1251
  updateSubagentPanelMessages(threadId, (panelMessages) =>
1234
- upsertStreamingMessage(panelMessages, {
1252
+ replaceStreamingMessage(panelMessages, {
1235
1253
  ...nextMessage,
1236
1254
  pending: false
1237
1255
  })
@@ -1433,6 +1451,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1433
1451
  status.value = 'streaming'
1434
1452
  return
1435
1453
  }
1454
+ clearThinkingPlaceholderForVisibleItem(params.item)
1436
1455
  seedStreamingMessage(params.item)
1437
1456
  status.value = 'streaming'
1438
1457
  return
@@ -1442,6 +1461,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1442
1461
  if (params.item.type === 'collabAgentToolCall') {
1443
1462
  applySubagentActivityItem(params.item)
1444
1463
  }
1464
+ clearThinkingPlaceholderForVisibleItem(params.item)
1445
1465
  for (const nextMessage of itemToMessages(params.item)) {
1446
1466
  const confirmedMessage = {
1447
1467
  ...nextMessage,
@@ -1452,12 +1472,13 @@ const applyNotification = (notification: CodexRpcNotification) => {
1452
1472
  continue
1453
1473
  }
1454
1474
 
1455
- messages.value = upsertStreamingMessage(messages.value, confirmedMessage)
1475
+ messages.value = replaceStreamingMessage(messages.value, confirmedMessage)
1456
1476
  }
1457
1477
  return
1458
1478
  }
1459
1479
  case 'item/agentMessage/delta': {
1460
1480
  const params = notification.params as { itemId: string, delta: string }
1481
+ clearThinkingPlaceholder()
1461
1482
  appendTextPartDelta(params.itemId, params.delta, {
1462
1483
  id: params.itemId,
1463
1484
  role: 'assistant',
@@ -1473,6 +1494,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1473
1494
  }
1474
1495
  case 'item/plan/delta': {
1475
1496
  const params = notification.params as { itemId: string, delta: string }
1497
+ clearThinkingPlaceholder()
1476
1498
  appendTextPartDelta(params.itemId, params.delta, {
1477
1499
  id: params.itemId,
1478
1500
  role: 'assistant',
@@ -1489,6 +1511,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1489
1511
  case 'item/reasoning/textDelta':
1490
1512
  case 'item/reasoning/summaryTextDelta': {
1491
1513
  const params = notification.params as { itemId: string, delta: string }
1514
+ clearThinkingPlaceholder()
1492
1515
  updateMessage(params.itemId, {
1493
1516
  id: params.itemId,
1494
1517
  role: 'assistant',
@@ -1596,6 +1619,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1596
1619
  case 'error': {
1597
1620
  const params = notification.params as { error?: { message?: string } }
1598
1621
  const messageText = params.error?.message ?? 'The stream failed.'
1622
+ clearThinkingPlaceholder()
1599
1623
  pushEventMessage('stream.error', messageText)
1600
1624
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1601
1625
  clearLiveStream(new Error(messageText))
@@ -1606,6 +1630,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1606
1630
  case 'turn/failed': {
1607
1631
  const params = notification.params as { error?: { message?: string } }
1608
1632
  const messageText = params.error?.message ?? 'The turn failed.'
1633
+ clearThinkingPlaceholder()
1609
1634
  pushEventMessage('turn.failed', messageText)
1610
1635
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1611
1636
  clearLiveStream(new Error(messageText))
@@ -1616,6 +1641,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1616
1641
  case 'stream/error': {
1617
1642
  const params = notification.params as { message?: string }
1618
1643
  const messageText = params.message ?? 'The stream failed.'
1644
+ clearThinkingPlaceholder()
1619
1645
  pushEventMessage('stream.error', messageText)
1620
1646
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1621
1647
  clearLiveStream(new Error(messageText))
@@ -1624,6 +1650,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1624
1650
  return
1625
1651
  }
1626
1652
  case 'turn/completed': {
1653
+ clearThinkingPlaceholder()
1627
1654
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1628
1655
  error.value = null
1629
1656
  status.value = 'ready'
@@ -1664,6 +1691,7 @@ const sendMessage = async () => {
1664
1691
  const optimisticMessageId = optimisticMessage.id
1665
1692
  rememberOptimisticAttachments(optimisticMessageId, submittedAttachments)
1666
1693
  messages.value = [...messages.value, optimisticMessage]
1694
+ ensureThinkingPlaceholder()
1667
1695
  let startedLiveStream: LiveStream | null = null
1668
1696
 
1669
1697
  try {
@@ -1681,6 +1709,7 @@ const sendMessage = async () => {
1681
1709
  input: buildTurnStartInput(text, uploadedAttachments),
1682
1710
  ...buildTurnOverrides(selectedModel.value, selectedEffort.value)
1683
1711
  })
1712
+ tokenUsage.value = null
1684
1713
  return
1685
1714
  }
1686
1715
 
@@ -1698,6 +1727,7 @@ const sendMessage = async () => {
1698
1727
  ...buildTurnOverrides(selectedModel.value, selectedEffort.value)
1699
1728
  })
1700
1729
 
1730
+ tokenUsage.value = null
1701
1731
  setLiveStreamTurnId(liveStream, turnStart.turn.id)
1702
1732
 
1703
1733
  for (const notification of liveStream.bufferedNotifications.splice(0, liveStream.bufferedNotifications.length)) {
@@ -1711,6 +1741,7 @@ const sendMessage = async () => {
1711
1741
  } catch (caughtError) {
1712
1742
  const messageText = caughtError instanceof Error ? caughtError.message : String(caughtError)
1713
1743
 
1744
+ clearThinkingPlaceholder()
1714
1745
  untrackPendingUserMessage(optimisticMessageId)
1715
1746
  removeOptimisticMessage(optimisticMessageId)
1716
1747
 
@@ -1932,7 +1963,6 @@ watch([selectedModel, availableModels], () => {
1932
1963
 
1933
1964
  <div class="sticky bottom-0 shrink-0 border-t border-default bg-default/95 px-4 py-3 backdrop-blur md:px-6">
1934
1965
  <div class="mx-auto w-full max-w-5xl">
1935
- <TunnelNotice class="mb-3" />
1936
1966
  <UAlert
1937
1967
  v-if="composerError"
1938
1968
  color="error"
@@ -2062,6 +2092,7 @@ watch([selectedModel, availableModels], () => {
2062
2092
 
2063
2093
  <div class="ml-auto flex shrink-0 items-center">
2064
2094
  <UPopover
2095
+ v-if="showContextIndicator"
2065
2096
  :content="{ side: 'top', align: 'end' }"
2066
2097
  arrow
2067
2098
  >
@@ -2101,8 +2132,6 @@ watch([selectedModel, availableModels], () => {
2101
2132
  {{ contextIndicatorLabel }}
2102
2133
  </span>
2103
2134
  </span>
2104
-
2105
- <span class="text-[11px] leading-none text-muted">context window</span>
2106
2135
  </button>
2107
2136
 
2108
2137
  <template #content>
@@ -2116,10 +2145,7 @@ watch([selectedModel, availableModels], () => {
2116
2145
  </div>
2117
2146
  </div>
2118
2147
 
2119
- <div
2120
- v-if="contextWindowState.contextWindow && contextWindowState.usedTokens !== null"
2121
- class="grid grid-cols-2 gap-3 text-sm"
2122
- >
2148
+ <div class="grid grid-cols-2 gap-3 text-sm">
2123
2149
  <div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
2124
2150
  <div class="text-[11px] uppercase tracking-[0.18em] text-muted">
2125
2151
  Remaining
@@ -2139,23 +2165,11 @@ watch([selectedModel, availableModels], () => {
2139
2165
  {{ formatCompactTokenCount(contextWindowState.usedTokens ?? 0) }}
2140
2166
  </div>
2141
2167
  <div class="text-xs text-muted">
2142
- of {{ formatCompactTokenCount(contextWindowState.contextWindow) }}
2168
+ of {{ formatCompactTokenCount(contextWindowState.contextWindow ?? 0) }}
2143
2169
  </div>
2144
2170
  </div>
2145
2171
  </div>
2146
2172
 
2147
- <div
2148
- v-else
2149
- class="rounded-2xl border border-default bg-elevated/35 px-3 py-2 text-sm text-muted"
2150
- >
2151
- <div v-if="contextWindowState.contextWindow">
2152
- Live token usage will appear after the next turn completes.
2153
- </div>
2154
- <div v-else>
2155
- Context window details are not available from the runtime yet.
2156
- </div>
2157
- </div>
2158
-
2159
2173
  <div class="grid grid-cols-2 gap-3 text-sm">
2160
2174
  <div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
2161
2175
  <div class="text-[11px] uppercase tracking-[0.18em] text-muted">
@@ -6,6 +6,8 @@ import type {
6
6
  ProjectRecord,
7
7
  ProjectResponse,
8
8
  ProjectsResponse,
9
+ ServiceUpdateResponse,
10
+ ServiceUpdateStatus,
9
11
  StartProjectResult
10
12
  } from '~~/shared/codori'
11
13
 
@@ -16,8 +18,16 @@ const mergeProject = (projects: ProjectRecord[], nextProject: ProjectRecord) =>
16
18
 
17
19
  export const useProjects = () => {
18
20
  const projects = useState<ProjectRecord[]>('codori-projects', () => [])
21
+ const serviceUpdate = useState<ServiceUpdateStatus>('codori-service-update', () => ({
22
+ enabled: false,
23
+ updateAvailable: false,
24
+ updating: false,
25
+ installedVersion: null,
26
+ latestVersion: null
27
+ }))
19
28
  const loaded = useState<boolean>('codori-projects-loaded', () => false)
20
29
  const loading = useState<boolean>('codori-projects-loading', () => false)
30
+ const serviceUpdatePending = useState<boolean>('codori-service-update-pending', () => false)
21
31
  const pendingProjectId = useState<string | null>('codori-projects-pending-id', () => null)
22
32
  const error = useState<string | null>('codori-projects-error', () => null)
23
33
  const configuredBase = String(useRuntimeConfig().public.serverBase ?? '')
@@ -36,6 +46,14 @@ export const useProjects = () => {
36
46
  loading.value = true
37
47
  error.value = null
38
48
  try {
49
+ void $fetch<ServiceUpdateResponse>(toApiUrl('/service/update'))
50
+ .then((response) => {
51
+ serviceUpdate.value = response.serviceUpdate
52
+ })
53
+ .catch(() => {
54
+ // Keep project discovery responsive even if the update check stalls or fails.
55
+ })
56
+
39
57
  const response = await $fetch<ProjectsResponse>(toApiUrl('/projects'))
40
58
  projects.value = response.projects
41
59
  loaded.value = true
@@ -87,13 +105,37 @@ export const useProjects = () => {
87
105
  return projects.value.find((project: ProjectRecord) => project.projectId === projectId) ?? null
88
106
  }
89
107
 
108
+ const triggerServiceUpdate = async () => {
109
+ if (serviceUpdatePending.value || serviceUpdate.value.updating) {
110
+ return serviceUpdate.value
111
+ }
112
+
113
+ serviceUpdatePending.value = true
114
+ error.value = null
115
+ try {
116
+ const response = await $fetch<ServiceUpdateResponse>(toApiUrl('/service/update'), {
117
+ method: 'POST'
118
+ })
119
+ serviceUpdate.value = response.serviceUpdate
120
+ return response.serviceUpdate
121
+ } catch (caughtError) {
122
+ error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
123
+ return serviceUpdate.value
124
+ } finally {
125
+ serviceUpdatePending.value = false
126
+ }
127
+ }
128
+
90
129
  return {
91
130
  projects,
131
+ serviceUpdate,
92
132
  loaded,
93
133
  loading,
134
+ serviceUpdatePending,
94
135
  error,
95
136
  pendingProjectId,
96
137
  refreshProjects,
138
+ triggerServiceUpdate,
97
139
  startProject,
98
140
  stopProject,
99
141
  getProject
@@ -1,7 +1,27 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue'
3
+ import { useProjects } from '../composables/useProjects'
3
4
 
4
5
  const sidebarCollapsed = ref(false)
6
+ const { serviceUpdate, serviceUpdatePending, triggerServiceUpdate } = useProjects()
7
+
8
+ const showServiceUpdateButton = computed(() =>
9
+ serviceUpdate.value.enabled && (serviceUpdate.value.updateAvailable || serviceUpdate.value.updating)
10
+ )
11
+
12
+ const serviceUpdateLabel = computed(() =>
13
+ serviceUpdate.value.updating ? 'Updating' : 'Update'
14
+ )
15
+
16
+ const serviceUpdateTooltip = computed(() => {
17
+ if (!serviceUpdate.value.latestVersion || !serviceUpdate.value.installedVersion) {
18
+ return serviceUpdate.value.updating ? 'Applying the latest server package update.' : 'Install the latest @codori/server package.'
19
+ }
20
+
21
+ return serviceUpdate.value.updating
22
+ ? `Updating @codori/server ${serviceUpdate.value.installedVersion} -> ${serviceUpdate.value.latestVersion}`
23
+ : `Update @codori/server ${serviceUpdate.value.installedVersion} -> ${serviceUpdate.value.latestVersion}`
24
+ })
5
25
 
6
26
  const sidebarUi = computed(() =>
7
27
  sidebarCollapsed.value
@@ -46,13 +66,44 @@ const sidebarUi = computed(() =>
46
66
  class="size-5"
47
67
  />
48
68
  </div>
49
- <div v-if="!collapsed">
50
- <div class="text-sm font-semibold">
51
- Codori
69
+ <div
70
+ v-if="!collapsed"
71
+ class="flex min-w-0 flex-1 items-start justify-between gap-3"
72
+ >
73
+ <div class="min-w-0">
74
+ <div class="text-sm font-semibold">
75
+ Codori
76
+ </div>
77
+ <div class="text-xs text-muted">
78
+ Codex project control
79
+ </div>
52
80
  </div>
53
- <div class="text-xs text-muted">
81
+ <UTooltip
82
+ v-if="showServiceUpdateButton"
83
+ :text="serviceUpdateTooltip"
84
+ >
85
+ <UButton
86
+ color="neutral"
87
+ variant="outline"
88
+ size="xs"
89
+ :loading="serviceUpdatePending || serviceUpdate.updating"
90
+ :disabled="serviceUpdatePending || serviceUpdate.updating"
91
+ @click="triggerServiceUpdate"
92
+ >
93
+ {{ serviceUpdateLabel }}
94
+ </UButton>
95
+ </UTooltip>
96
+ </div>
97
+ <div
98
+ v-else-if="collapsed"
99
+ class="sr-only"
100
+ >
101
+ <span>
102
+ Codori
103
+ </span>
104
+ <span>
54
105
  Codex project control
55
- </div>
106
+ </span>
56
107
  </div>
57
108
  </div>
58
109
  </template>
@@ -54,7 +54,6 @@
54
54
  </div>
55
55
  </div>
56
56
 
57
- <TunnelNotice class="w-full text-left" />
58
57
  </div>
59
58
  </div>
60
59
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codori/client",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "description": "Codori Nuxt dashboard for project browsing, Codex chat, and thread resume.",
6
6
  "type": "module",
@@ -0,0 +1,7 @@
1
+ import { defineEventHandler } from 'h3'
2
+ import type { ServiceUpdateResponse } from '~~/shared/codori'
3
+ import { proxyServerRequest } from '../../../utils/server-proxy'
4
+
5
+ export default defineEventHandler(async (event) =>
6
+ await proxyServerRequest<ServiceUpdateResponse>(event, '/api/service/update')
7
+ )
@@ -0,0 +1,9 @@
1
+ import { defineEventHandler } from 'h3'
2
+ import type { ServiceUpdateResponse } from '~~/shared/codori'
3
+ import { proxyServerRequest } from '../../../utils/server-proxy'
4
+
5
+ export default defineEventHandler(async (event) =>
6
+ await proxyServerRequest<ServiceUpdateResponse>(event, '/api/service/update', {
7
+ method: 'POST'
8
+ })
9
+ )
@@ -20,15 +20,26 @@ export type ModelOption = {
20
20
  }
21
21
 
22
22
  export type TokenUsageSnapshot = {
23
+ totalTokens: number | null
23
24
  totalInputTokens: number
24
25
  totalCachedInputTokens: number
25
26
  totalOutputTokens: number
27
+ lastUsageKnown: boolean
28
+ lastTotalTokens: number | null
26
29
  lastInputTokens: number
27
30
  lastCachedInputTokens: number
28
31
  lastOutputTokens: number
29
32
  modelContextWindow: number | null
30
33
  }
31
34
 
35
+ export type ContextWindowState = {
36
+ contextWindow: number | null
37
+ usedTokens: number | null
38
+ remainingTokens: number | null
39
+ usedPercent: number | null
40
+ remainingPercent: number | null
41
+ }
42
+
32
43
  type ReasoningEffortOptionRecord = {
33
44
  reasoningEffort?: unknown
34
45
  }
@@ -250,15 +261,18 @@ export const normalizeThreadTokenUsage = (value: unknown): TokenUsageSnapshot |
250
261
  }
251
262
 
252
263
  const total = isObjectRecord(tokenUsage.total) ? tokenUsage.total : {}
253
- const last = isObjectRecord(tokenUsage.last) ? tokenUsage.last : {}
264
+ const last = isObjectRecord(tokenUsage.last) ? tokenUsage.last : null
254
265
 
255
266
  return {
267
+ totalTokens: toFiniteNumber(total.totalTokens),
256
268
  totalInputTokens: toFiniteNumber(total.inputTokens) ?? 0,
257
269
  totalCachedInputTokens: toFiniteNumber(total.cachedInputTokens) ?? 0,
258
270
  totalOutputTokens: toFiniteNumber(total.outputTokens) ?? 0,
259
- lastInputTokens: toFiniteNumber(last.inputTokens) ?? 0,
260
- lastCachedInputTokens: toFiniteNumber(last.cachedInputTokens) ?? 0,
261
- lastOutputTokens: toFiniteNumber(last.outputTokens) ?? 0,
271
+ lastUsageKnown: last !== null,
272
+ lastTotalTokens: toFiniteNumber(last?.totalTokens),
273
+ lastInputTokens: toFiniteNumber(last?.inputTokens) ?? 0,
274
+ lastCachedInputTokens: toFiniteNumber(last?.cachedInputTokens) ?? 0,
275
+ lastOutputTokens: toFiniteNumber(last?.outputTokens) ?? 0,
262
276
  modelContextWindow: toFiniteNumber(tokenUsage.modelContextWindow)
263
277
  }
264
278
  }
@@ -306,15 +320,16 @@ export const formatCompactTokenCount = (value: number) => {
306
320
 
307
321
  export const resolveContextWindowState = (
308
322
  tokenUsage: TokenUsageSnapshot | null,
309
- fallbackContextWindow: number | null,
310
- usageKnown = true
311
- ) => {
323
+ fallbackContextWindow: number | null
324
+ ): ContextWindowState => {
312
325
  const contextWindow = tokenUsage?.modelContextWindow ?? fallbackContextWindow
326
+ // App-server exposes cumulative thread totals separately; the latest turn total
327
+ // is the closest match to current context occupancy.
313
328
  const usedTokens = tokenUsage
314
- ? tokenUsage.totalInputTokens + tokenUsage.totalOutputTokens
315
- : usageKnown
316
- ? 0
317
- : null
329
+ ? tokenUsage.lastTotalTokens ?? (tokenUsage.lastUsageKnown
330
+ ? tokenUsage.lastInputTokens + tokenUsage.lastOutputTokens
331
+ : null)
332
+ : null
318
333
 
319
334
  if (!contextWindow || usedTokens == null) {
320
335
  return {
@@ -337,3 +352,6 @@ export const resolveContextWindowState = (
337
352
  remainingPercent: Math.max(0, 100 - usedPercent)
338
353
  }
339
354
  }
355
+
356
+ export const shouldShowContextWindowIndicator = (state: ContextWindowState) =>
357
+ state.contextWindow !== null && state.usedTokens !== null
@@ -2,6 +2,7 @@ import type { CodexThread, CodexThreadItem, CodexUserInput } from './codex-rpc'
2
2
 
3
3
  export const EVENT_PART = 'data-thread-event' as const
4
4
  export const ITEM_PART = 'data-thread-item' as const
5
+ export const THINKING_PLACEHOLDER_MESSAGE_ID = 'assistant-thinking-placeholder'
5
6
 
6
7
  export type ThreadEventData =
7
8
  | {
@@ -336,11 +337,13 @@ const normalizeParts = (message: ChatMessage): ChatPart[] =>
336
337
  return part
337
338
  })
338
339
 
340
+ const normalizeMessage = (message: ChatMessage): ChatMessage => ({
341
+ ...message,
342
+ parts: normalizeParts(message)
343
+ })
344
+
339
345
  export const upsertStreamingMessage = (messages: ChatMessage[], nextMessage: ChatMessage) => {
340
- const normalizedMessage = {
341
- ...nextMessage,
342
- parts: normalizeParts(nextMessage)
343
- }
346
+ const normalizedMessage = normalizeMessage(nextMessage)
344
347
  const nextMessages = messages.slice()
345
348
  const existingIndex = nextMessages.findIndex(message => message.id === normalizedMessage.id)
346
349
 
@@ -360,3 +363,43 @@ export const upsertStreamingMessage = (messages: ChatMessage[], nextMessage: Cha
360
363
 
361
364
  return nextMessages
362
365
  }
366
+
367
+ export const replaceStreamingMessage = (messages: ChatMessage[], nextMessage: ChatMessage) => {
368
+ const normalizedMessage = normalizeMessage(nextMessage)
369
+ const nextMessages = messages.slice()
370
+ const existingIndex = nextMessages.findIndex(message => message.id === normalizedMessage.id)
371
+
372
+ if (existingIndex === -1) {
373
+ nextMessages.push(normalizedMessage)
374
+ return nextMessages
375
+ }
376
+
377
+ nextMessages.splice(existingIndex, 1, normalizedMessage)
378
+ return nextMessages
379
+ }
380
+
381
+ export const buildThinkingPlaceholderMessage = (): ChatMessage => ({
382
+ id: THINKING_PLACEHOLDER_MESSAGE_ID,
383
+ role: 'assistant',
384
+ pending: true,
385
+ parts: [{
386
+ type: 'reasoning',
387
+ summary: ['Thinking...'],
388
+ content: [],
389
+ state: 'streaming'
390
+ }]
391
+ })
392
+
393
+ export const showThinkingPlaceholder = (messages: ChatMessage[]) =>
394
+ upsertStreamingMessage(messages, buildThinkingPlaceholderMessage())
395
+
396
+ export const hideThinkingPlaceholder = (messages: ChatMessage[]) => {
397
+ const index = messages.findIndex(message => message.id === THINKING_PLACEHOLDER_MESSAGE_ID)
398
+ if (index === -1) {
399
+ return messages
400
+ }
401
+
402
+ const nextMessages = messages.slice()
403
+ nextMessages.splice(index, 1)
404
+ return nextMessages
405
+ }
package/shared/codori.ts CHANGED
@@ -7,6 +7,10 @@ export type ProjectRecord = {
7
7
  pid: number | null
8
8
  port: number | null
9
9
  startedAt: number | null
10
+ lastActivityAt: number | null
11
+ activeSessionCount: number
12
+ idleTimeoutMs: number | null
13
+ idleDeadlineAt: number | null
10
14
  error: string | null
11
15
  }
12
16
 
@@ -14,6 +18,14 @@ export type StartProjectResult = ProjectRecord & {
14
18
  reusedExisting: boolean
15
19
  }
16
20
 
21
+ export type ServiceUpdateStatus = {
22
+ enabled: boolean
23
+ updateAvailable: boolean
24
+ updating: boolean
25
+ installedVersion: string | null
26
+ latestVersion: string | null
27
+ }
28
+
17
29
  export type ProjectsResponse = {
18
30
  projects: ProjectRecord[]
19
31
  }
@@ -22,6 +34,10 @@ export type ProjectResponse = {
22
34
  project: ProjectRecord | StartProjectResult
23
35
  }
24
36
 
37
+ export type ServiceUpdateResponse = {
38
+ serviceUpdate: ServiceUpdateStatus
39
+ }
40
+
25
41
  export const normalizeProjectIdParam = (value: string | string[] | undefined) => {
26
42
  if (!value) {
27
43
  return null
@@ -1,27 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
-
4
- const localHostnames = new Set([
5
- 'localhost',
6
- '127.0.0.1',
7
- '::1'
8
- ])
9
-
10
- const hostname = typeof window === 'undefined' ? 'localhost' : window.location.hostname
11
-
12
- const shouldShow = computed(() => !localHostnames.has(hostname))
13
- </script>
14
-
15
- <template>
16
- <UAlert
17
- v-if="shouldShow"
18
- color="warning"
19
- variant="soft"
20
- icon="i-lucide-shield-alert"
21
- title="Private tunnel is not included"
22
- >
23
- <template #description>
24
- Codori does not create a private tunnel for you. Expose this service through your own network layer such as Tailscale or Cloudflare Tunnel.
25
- </template>
26
- </UAlert>
27
- </template>