@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.
- package/app/components/ChatWorkspace.vue +38 -24
- package/app/composables/useProjects.ts +42 -0
- package/app/layouts/default.vue +56 -5
- package/app/pages/index.vue +0 -1
- package/package.json +1 -1
- package/server/api/codori/service/update.get.ts +7 -0
- package/server/api/codori/service/update.post.ts +9 -0
- package/shared/chat-prompt-controls.ts +29 -11
- package/shared/codex-chat.ts +47 -4
- package/shared/codori.ts +16 -0
- package/app/components/TunnelNotice.vue +0 -27
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
package/app/layouts/default.vue
CHANGED
|
@@ -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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
106
|
+
</span>
|
|
56
107
|
</div>
|
|
57
108
|
</div>
|
|
58
109
|
</template>
|
package/app/pages/index.vue
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
package/shared/codex-chat.ts
CHANGED
|
@@ -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>
|