@codori/client 0.0.3 → 0.0.4

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,
@@ -505,6 +508,20 @@ const removeOptimisticMessage = (messageId: string) => {
505
508
  optimisticAttachmentSnapshots.delete(messageId)
506
509
  }
507
510
 
511
+ const clearThinkingPlaceholder = () => {
512
+ messages.value = hideThinkingPlaceholder(messages.value)
513
+ }
514
+
515
+ const ensureThinkingPlaceholder = () => {
516
+ messages.value = showThinkingPlaceholder(messages.value)
517
+ }
518
+
519
+ const clearThinkingPlaceholderForVisibleItem = (item: CodexThreadItem) => {
520
+ if (item.type !== 'userMessage') {
521
+ clearThinkingPlaceholder()
522
+ }
523
+ }
524
+
508
525
  const restoreDraftIfPristine = (text: string, submittedAttachments: DraftAttachment[]) => {
509
526
  if (!input.value.trim()) {
510
527
  input.value = text
@@ -1231,7 +1248,7 @@ const applySubagentNotification = (threadId: string, notification: CodexRpcNotif
1231
1248
  }
1232
1249
  for (const nextMessage of itemToMessages(params.item)) {
1233
1250
  updateSubagentPanelMessages(threadId, (panelMessages) =>
1234
- upsertStreamingMessage(panelMessages, {
1251
+ replaceStreamingMessage(panelMessages, {
1235
1252
  ...nextMessage,
1236
1253
  pending: false
1237
1254
  })
@@ -1433,6 +1450,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1433
1450
  status.value = 'streaming'
1434
1451
  return
1435
1452
  }
1453
+ clearThinkingPlaceholderForVisibleItem(params.item)
1436
1454
  seedStreamingMessage(params.item)
1437
1455
  status.value = 'streaming'
1438
1456
  return
@@ -1442,6 +1460,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1442
1460
  if (params.item.type === 'collabAgentToolCall') {
1443
1461
  applySubagentActivityItem(params.item)
1444
1462
  }
1463
+ clearThinkingPlaceholderForVisibleItem(params.item)
1445
1464
  for (const nextMessage of itemToMessages(params.item)) {
1446
1465
  const confirmedMessage = {
1447
1466
  ...nextMessage,
@@ -1452,12 +1471,13 @@ const applyNotification = (notification: CodexRpcNotification) => {
1452
1471
  continue
1453
1472
  }
1454
1473
 
1455
- messages.value = upsertStreamingMessage(messages.value, confirmedMessage)
1474
+ messages.value = replaceStreamingMessage(messages.value, confirmedMessage)
1456
1475
  }
1457
1476
  return
1458
1477
  }
1459
1478
  case 'item/agentMessage/delta': {
1460
1479
  const params = notification.params as { itemId: string, delta: string }
1480
+ clearThinkingPlaceholder()
1461
1481
  appendTextPartDelta(params.itemId, params.delta, {
1462
1482
  id: params.itemId,
1463
1483
  role: 'assistant',
@@ -1473,6 +1493,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1473
1493
  }
1474
1494
  case 'item/plan/delta': {
1475
1495
  const params = notification.params as { itemId: string, delta: string }
1496
+ clearThinkingPlaceholder()
1476
1497
  appendTextPartDelta(params.itemId, params.delta, {
1477
1498
  id: params.itemId,
1478
1499
  role: 'assistant',
@@ -1489,6 +1510,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1489
1510
  case 'item/reasoning/textDelta':
1490
1511
  case 'item/reasoning/summaryTextDelta': {
1491
1512
  const params = notification.params as { itemId: string, delta: string }
1513
+ clearThinkingPlaceholder()
1492
1514
  updateMessage(params.itemId, {
1493
1515
  id: params.itemId,
1494
1516
  role: 'assistant',
@@ -1596,6 +1618,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1596
1618
  case 'error': {
1597
1619
  const params = notification.params as { error?: { message?: string } }
1598
1620
  const messageText = params.error?.message ?? 'The stream failed.'
1621
+ clearThinkingPlaceholder()
1599
1622
  pushEventMessage('stream.error', messageText)
1600
1623
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1601
1624
  clearLiveStream(new Error(messageText))
@@ -1606,6 +1629,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1606
1629
  case 'turn/failed': {
1607
1630
  const params = notification.params as { error?: { message?: string } }
1608
1631
  const messageText = params.error?.message ?? 'The turn failed.'
1632
+ clearThinkingPlaceholder()
1609
1633
  pushEventMessage('turn.failed', messageText)
1610
1634
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1611
1635
  clearLiveStream(new Error(messageText))
@@ -1616,6 +1640,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1616
1640
  case 'stream/error': {
1617
1641
  const params = notification.params as { message?: string }
1618
1642
  const messageText = params.message ?? 'The stream failed.'
1643
+ clearThinkingPlaceholder()
1619
1644
  pushEventMessage('stream.error', messageText)
1620
1645
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1621
1646
  clearLiveStream(new Error(messageText))
@@ -1624,6 +1649,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
1624
1649
  return
1625
1650
  }
1626
1651
  case 'turn/completed': {
1652
+ clearThinkingPlaceholder()
1627
1653
  clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
1628
1654
  error.value = null
1629
1655
  status.value = 'ready'
@@ -1664,6 +1690,7 @@ const sendMessage = async () => {
1664
1690
  const optimisticMessageId = optimisticMessage.id
1665
1691
  rememberOptimisticAttachments(optimisticMessageId, submittedAttachments)
1666
1692
  messages.value = [...messages.value, optimisticMessage]
1693
+ ensureThinkingPlaceholder()
1667
1694
  let startedLiveStream: LiveStream | null = null
1668
1695
 
1669
1696
  try {
@@ -1711,6 +1738,7 @@ const sendMessage = async () => {
1711
1738
  } catch (caughtError) {
1712
1739
  const messageText = caughtError instanceof Error ? caughtError.message : String(caughtError)
1713
1740
 
1741
+ clearThinkingPlaceholder()
1714
1742
  untrackPendingUserMessage(optimisticMessageId)
1715
1743
  removeOptimisticMessage(optimisticMessageId)
1716
1744
 
@@ -1932,7 +1960,6 @@ watch([selectedModel, availableModels], () => {
1932
1960
 
1933
1961
  <div class="sticky bottom-0 shrink-0 border-t border-default bg-default/95 px-4 py-3 backdrop-blur md:px-6">
1934
1962
  <div class="mx-auto w-full max-w-5xl">
1935
- <TunnelNotice class="mb-3" />
1936
1963
  <UAlert
1937
1964
  v-if="composerError"
1938
1965
  color="error"
@@ -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.4",
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
+ )
@@ -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
@@ -14,6 +14,14 @@ export type StartProjectResult = ProjectRecord & {
14
14
  reusedExisting: boolean
15
15
  }
16
16
 
17
+ export type ServiceUpdateStatus = {
18
+ enabled: boolean
19
+ updateAvailable: boolean
20
+ updating: boolean
21
+ installedVersion: string | null
22
+ latestVersion: string | null
23
+ }
24
+
17
25
  export type ProjectsResponse = {
18
26
  projects: ProjectRecord[]
19
27
  }
@@ -22,6 +30,10 @@ export type ProjectResponse = {
22
30
  project: ProjectRecord | StartProjectResult
23
31
  }
24
32
 
33
+ export type ServiceUpdateResponse = {
34
+ serviceUpdate: ServiceUpdateStatus
35
+ }
36
+
25
37
  export const normalizeProjectIdParam = (value: string | string[] | undefined) => {
26
38
  if (!value) {
27
39
  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>