kanna-code 0.32.2 → 0.32.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.
- package/dist/client/assets/index-CCCDQbjU.js +2598 -0
- package/dist/client/assets/index-DbTkHYDv.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/cli-runtime.ts +9 -7
- package/src/server/event-store.test.ts +74 -2
- package/src/server/event-store.ts +120 -16
- package/src/server/events.ts +0 -7
- package/src/server/provider-catalog.test.ts +6 -0
- package/src/server/provider-catalog.ts +4 -2
- package/src/server/read-models.test.ts +3 -5
- package/src/server/read-models.ts +6 -2
- package/src/server/share.ts +13 -42
- package/src/server/ws-router.test.ts +5 -1
- package/src/server/ws-router.ts +9 -1
- package/src/shared/dev-ports.ts +5 -7
- package/src/shared/share.ts +15 -0
- package/src/shared/types.test.ts +24 -0
- package/src/shared/types.ts +60 -25
- package/dist/client/assets/index-B3EUEMfY.css +0 -32
- package/dist/client/assets/index-CdBLcsbf.js +0 -2598
|
@@ -20,6 +20,25 @@ import { resolveLocalPath } from "./paths"
|
|
|
20
20
|
|
|
21
21
|
const COMPACTION_THRESHOLD_BYTES = 2 * 1024 * 1024
|
|
22
22
|
const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
|
|
23
|
+
const SIDEBAR_PROJECT_ORDER_FILE = "sidebar-order.json"
|
|
24
|
+
|
|
25
|
+
function normalizeSidebarProjectOrder(value: unknown) {
|
|
26
|
+
if (!Array.isArray(value)) {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const seen = new Set<string>()
|
|
31
|
+
const projectIds: string[] = []
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
if (typeof entry !== "string") continue
|
|
34
|
+
const projectId = entry.trim()
|
|
35
|
+
if (!projectId || seen.has(projectId)) continue
|
|
36
|
+
seen.add(projectId)
|
|
37
|
+
projectIds.push(projectId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return projectIds
|
|
41
|
+
}
|
|
23
42
|
|
|
24
43
|
function isSendToStartingProfilingEnabled() {
|
|
25
44
|
return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
|
|
@@ -59,7 +78,6 @@ function getReplayEventPriority(event: StoreEvent) {
|
|
|
59
78
|
switch (event.type) {
|
|
60
79
|
case "project_opened":
|
|
61
80
|
case "project_removed":
|
|
62
|
-
case "sidebar_project_order_set":
|
|
63
81
|
return 0
|
|
64
82
|
case "chat_created":
|
|
65
83
|
return 1
|
|
@@ -124,7 +142,10 @@ export class EventStore {
|
|
|
124
142
|
private readonly queuedMessagesLogPath: string
|
|
125
143
|
private readonly turnsLogPath: string
|
|
126
144
|
private readonly transcriptsDir: string
|
|
145
|
+
private readonly sidebarProjectOrderPath: string
|
|
127
146
|
private legacyMessagesByChatId = new Map<string, TranscriptEntry[]>()
|
|
147
|
+
private legacySidebarProjectOrder: string[] = []
|
|
148
|
+
private sidebarProjectOrder: string[] = []
|
|
128
149
|
private snapshotHasLegacyMessages = false
|
|
129
150
|
private cachedTranscript: { chatId: string; entries: TranscriptEntry[] } | null = null
|
|
130
151
|
|
|
@@ -137,6 +158,7 @@ export class EventStore {
|
|
|
137
158
|
this.queuedMessagesLogPath = path.join(this.dataDir, "queued-messages.jsonl")
|
|
138
159
|
this.turnsLogPath = path.join(this.dataDir, "turns.jsonl")
|
|
139
160
|
this.transcriptsDir = path.join(this.dataDir, "transcripts")
|
|
161
|
+
this.sidebarProjectOrderPath = path.join(this.dataDir, SIDEBAR_PROJECT_ORDER_FILE)
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
async initialize() {
|
|
@@ -149,6 +171,7 @@ export class EventStore {
|
|
|
149
171
|
await this.ensureFile(this.turnsLogPath)
|
|
150
172
|
await this.loadSnapshot()
|
|
151
173
|
await this.replayLogs()
|
|
174
|
+
await this.loadSidebarProjectOrder()
|
|
152
175
|
if (!(await this.hasLegacyTranscriptData()) && await this.shouldCompact()) {
|
|
153
176
|
await this.compact()
|
|
154
177
|
}
|
|
@@ -199,7 +222,7 @@ export class EventStore {
|
|
|
199
222
|
unread: chat.unread ?? false,
|
|
200
223
|
})
|
|
201
224
|
}
|
|
202
|
-
this.
|
|
225
|
+
this.legacySidebarProjectOrder = normalizeSidebarProjectOrder(parsed.sidebarProjectOrder)
|
|
203
226
|
if (parsed.queuedMessages?.length) {
|
|
204
227
|
for (const queuedSet of parsed.queuedMessages) {
|
|
205
228
|
this.state.queuedMessagesByChatId.set(queuedSet.chatId, queuedSet.entries.map((entry) => ({
|
|
@@ -225,7 +248,8 @@ export class EventStore {
|
|
|
225
248
|
this.state.projectIdsByPath.clear()
|
|
226
249
|
this.state.chatsById.clear()
|
|
227
250
|
this.state.queuedMessagesByChatId.clear()
|
|
228
|
-
this.
|
|
251
|
+
this.sidebarProjectOrder = []
|
|
252
|
+
this.legacySidebarProjectOrder = []
|
|
229
253
|
this.cachedTranscript = null
|
|
230
254
|
}
|
|
231
255
|
|
|
@@ -234,6 +258,86 @@ export class EventStore {
|
|
|
234
258
|
this.snapshotHasLegacyMessages = false
|
|
235
259
|
}
|
|
236
260
|
|
|
261
|
+
private async loadSidebarProjectOrder() {
|
|
262
|
+
const file = Bun.file(this.sidebarProjectOrderPath)
|
|
263
|
+
if (await file.exists()) {
|
|
264
|
+
try {
|
|
265
|
+
const text = await file.text()
|
|
266
|
+
if (!text.trim()) {
|
|
267
|
+
this.sidebarProjectOrder = []
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
this.sidebarProjectOrder = normalizeSidebarProjectOrder(JSON.parse(text))
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.warn(`${LOG_PREFIX} Failed to load ${SIDEBAR_PROJECT_ORDER_FILE}, ignoring saved order:`, error)
|
|
273
|
+
this.sidebarProjectOrder = []
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const legacySidebarProjectOrder = await this.loadLegacySidebarProjectOrder()
|
|
279
|
+
this.sidebarProjectOrder = legacySidebarProjectOrder
|
|
280
|
+
if (legacySidebarProjectOrder.length > 0) {
|
|
281
|
+
await this.writeSidebarProjectOrderFile(legacySidebarProjectOrder)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async loadLegacySidebarProjectOrder() {
|
|
286
|
+
const fromProjectsLog = await this.readLegacySidebarProjectOrderFromProjectsLog()
|
|
287
|
+
if (fromProjectsLog.length > 0) {
|
|
288
|
+
return fromProjectsLog
|
|
289
|
+
}
|
|
290
|
+
return [...this.legacySidebarProjectOrder]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async readLegacySidebarProjectOrderFromProjectsLog() {
|
|
294
|
+
const file = Bun.file(this.projectsLogPath)
|
|
295
|
+
if (!(await file.exists())) return []
|
|
296
|
+
|
|
297
|
+
const text = await file.text()
|
|
298
|
+
if (!text.trim()) return []
|
|
299
|
+
|
|
300
|
+
const lines = text.split("\n")
|
|
301
|
+
let lastNonEmpty = -1
|
|
302
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
303
|
+
if (lines[index].trim()) {
|
|
304
|
+
lastNonEmpty = index
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let projectIds: string[] = []
|
|
310
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
311
|
+
const line = lines[index].trim()
|
|
312
|
+
if (!line) continue
|
|
313
|
+
try {
|
|
314
|
+
const event = JSON.parse(line) as {
|
|
315
|
+
v?: number
|
|
316
|
+
type?: string
|
|
317
|
+
projectIds?: unknown
|
|
318
|
+
}
|
|
319
|
+
if (event.v !== STORE_VERSION || event.type !== "sidebar_project_order_set") {
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
projectIds = normalizeSidebarProjectOrder(event.projectIds)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (index === lastNonEmpty) {
|
|
325
|
+
console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(this.projectsLogPath)} while migrating sidebar order`)
|
|
326
|
+
return projectIds
|
|
327
|
+
}
|
|
328
|
+
console.warn(`${LOG_PREFIX} Failed to migrate sidebar order from ${path.basename(this.projectsLogPath)}:`, error)
|
|
329
|
+
return []
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return projectIds
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async writeSidebarProjectOrderFile(projectIds: string[]) {
|
|
337
|
+
await mkdir(this.dataDir, { recursive: true })
|
|
338
|
+
await writeFile(this.sidebarProjectOrderPath, `${JSON.stringify(projectIds, null, 2)}\n`, "utf8")
|
|
339
|
+
}
|
|
340
|
+
|
|
237
341
|
private async replayLogs() {
|
|
238
342
|
if (this.storageReset) return
|
|
239
343
|
const replayEvents = [
|
|
@@ -283,6 +387,9 @@ export class EventStore {
|
|
|
283
387
|
await this.clearStorage()
|
|
284
388
|
return []
|
|
285
389
|
}
|
|
390
|
+
if ((event as { type?: unknown }).type === "sidebar_project_order_set") {
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
286
393
|
parsedEvents.push({
|
|
287
394
|
event: event as StoreEvent,
|
|
288
395
|
sourceIndex,
|
|
@@ -325,10 +432,6 @@ export class EventStore {
|
|
|
325
432
|
this.state.projectIdsByPath.delete(project.localPath)
|
|
326
433
|
break
|
|
327
434
|
}
|
|
328
|
-
case "sidebar_project_order_set": {
|
|
329
|
-
this.state.sidebarProjectOrder = [...event.projectIds]
|
|
330
|
-
break
|
|
331
|
-
}
|
|
332
435
|
case "chat_created": {
|
|
333
436
|
const chat = {
|
|
334
437
|
id: event.chatId,
|
|
@@ -541,7 +644,7 @@ export class EventStore {
|
|
|
541
644
|
})
|
|
542
645
|
|
|
543
646
|
const uniqueProjectIds = [...new Set(validProjectIds)]
|
|
544
|
-
const current = this.
|
|
647
|
+
const current = this.sidebarProjectOrder
|
|
545
648
|
if (
|
|
546
649
|
uniqueProjectIds.length === current.length
|
|
547
650
|
&& uniqueProjectIds.every((projectId, index) => current[index] === projectId)
|
|
@@ -549,13 +652,11 @@ export class EventStore {
|
|
|
549
652
|
return
|
|
550
653
|
}
|
|
551
654
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
await this.append(this.projectsLogPath, event)
|
|
655
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
656
|
+
await this.writeSidebarProjectOrderFile(uniqueProjectIds)
|
|
657
|
+
this.sidebarProjectOrder = [...uniqueProjectIds]
|
|
658
|
+
})
|
|
659
|
+
return this.writeChain
|
|
559
660
|
}
|
|
560
661
|
|
|
561
662
|
async createChat(projectId: string) {
|
|
@@ -830,6 +931,10 @@ export class EventStore {
|
|
|
830
931
|
return chat
|
|
831
932
|
}
|
|
832
933
|
|
|
934
|
+
getSidebarProjectOrder() {
|
|
935
|
+
return [...this.sidebarProjectOrder]
|
|
936
|
+
}
|
|
937
|
+
|
|
833
938
|
private getMessagesPageFromEntries(entries: TranscriptEntry[], limit: number, beforeIndex?: number): TranscriptPageResult {
|
|
834
939
|
if (entries.length === 0) {
|
|
835
940
|
return { entries: [], hasOlder: false, olderCursor: null }
|
|
@@ -961,7 +1066,6 @@ export class EventStore {
|
|
|
961
1066
|
v: STORE_VERSION,
|
|
962
1067
|
generatedAt: Date.now(),
|
|
963
1068
|
projects: this.listProjects().map((project) => ({ ...project })),
|
|
964
|
-
sidebarProjectOrder: [...this.state.sidebarProjectOrder],
|
|
965
1069
|
chats: [...this.state.chatsById.values()]
|
|
966
1070
|
.filter((chat) => !chat.deletedAt)
|
|
967
1071
|
.map((chat) => ({ ...chat })),
|
package/src/server/events.ts
CHANGED
|
@@ -25,7 +25,6 @@ export interface StoreState {
|
|
|
25
25
|
projectIdsByPath: Map<string, string>
|
|
26
26
|
chatsById: Map<string, ChatRecord>
|
|
27
27
|
queuedMessagesByChatId: Map<string, QueuedChatMessage[]>
|
|
28
|
-
sidebarProjectOrder: string[]
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export interface SnapshotFile {
|
|
@@ -50,11 +49,6 @@ export type ProjectEvent = {
|
|
|
50
49
|
type: "project_removed"
|
|
51
50
|
timestamp: number
|
|
52
51
|
projectId: string
|
|
53
|
-
} | {
|
|
54
|
-
v: 2
|
|
55
|
-
type: "sidebar_project_order_set"
|
|
56
|
-
timestamp: number
|
|
57
|
-
projectIds: string[]
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
export type ChatEvent =
|
|
@@ -167,7 +161,6 @@ export function createEmptyState(): StoreState {
|
|
|
167
161
|
projectIdsByPath: new Map(),
|
|
168
162
|
chatsById: new Map(),
|
|
169
163
|
queuedMessagesByChatId: new Map(),
|
|
170
|
-
sidebarProjectOrder: [],
|
|
171
164
|
}
|
|
172
165
|
}
|
|
173
166
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
codexServiceTierFromModelOptions,
|
|
4
4
|
normalizeClaudeModelOptions,
|
|
5
5
|
normalizeCodexModelOptions,
|
|
6
|
+
normalizeServerModel,
|
|
6
7
|
} from "./provider-catalog"
|
|
7
8
|
import { resolveClaudeApiModelId } from "../shared/types"
|
|
8
9
|
|
|
@@ -55,6 +56,11 @@ describe("provider catalog normalization", () => {
|
|
|
55
56
|
expect(codexServiceTierFromModelOptions(normalized)).toBe("fast")
|
|
56
57
|
})
|
|
57
58
|
|
|
59
|
+
test("normalizes server model ids through the shared alias catalog", () => {
|
|
60
|
+
expect(normalizeServerModel("claude", "opus")).toBe("claude-opus-4-7")
|
|
61
|
+
expect(normalizeServerModel("codex", "gpt-5-codex")).toBe("gpt-5.3-codex")
|
|
62
|
+
})
|
|
63
|
+
|
|
58
64
|
test("resolves Claude API model ids for 1m context window", () => {
|
|
59
65
|
expect(resolveClaudeApiModelId("claude-opus-4-7", "1m")).toBe("claude-opus-4-7[1m]")
|
|
60
66
|
expect(resolveClaudeApiModelId("claude-sonnet-4-6", "200k")).toBe("claude-sonnet-4-6")
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
DEFAULT_CODEX_MODEL_OPTIONS,
|
|
14
14
|
PROVIDERS,
|
|
15
15
|
normalizeClaudeContextWindow,
|
|
16
|
+
normalizeProviderModelId,
|
|
16
17
|
isClaudeReasoningEffort,
|
|
17
18
|
isCodexReasoningEffort,
|
|
18
19
|
} from "../shared/types"
|
|
@@ -43,8 +44,9 @@ export function getServerProviderCatalog(provider: AgentProvider): ProviderCatal
|
|
|
43
44
|
|
|
44
45
|
export function normalizeServerModel(provider: AgentProvider, model?: string): string {
|
|
45
46
|
const catalog = getServerProviderCatalog(provider)
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const normalizedModel = normalizeProviderModelId(provider, model, catalog.defaultModel)
|
|
48
|
+
if (catalog.models.some((candidate) => candidate.id === normalizedModel)) {
|
|
49
|
+
return normalizedModel
|
|
48
50
|
}
|
|
49
51
|
return catalog.defaultModel
|
|
50
52
|
}
|
|
@@ -26,7 +26,7 @@ describe("read models", () => {
|
|
|
26
26
|
lastTurnOutcome: null,
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
const sidebar = deriveSidebarData(state, new Map(), 1_000_000)
|
|
29
|
+
const sidebar = deriveSidebarData(state, new Map(), { nowMs: 1_000_000 })
|
|
30
30
|
expect(sidebar.projectGroups[0]?.chats[0]?.provider).toBe("codex")
|
|
31
31
|
expect(sidebar.projectGroups[0]?.chats[0]?.unread).toBe(true)
|
|
32
32
|
expect(sidebar.projectGroups[0]?.previewChats.map((chat) => chat.chatId)).toEqual(["chat-1"])
|
|
@@ -198,9 +198,7 @@ describe("read models", () => {
|
|
|
198
198
|
createdAt: 3,
|
|
199
199
|
updatedAt: 15,
|
|
200
200
|
})
|
|
201
|
-
state
|
|
202
|
-
|
|
203
|
-
const sidebar = deriveSidebarData(state, new Map())
|
|
201
|
+
const sidebar = deriveSidebarData(state, new Map(), { sidebarProjectOrder: ["project-1"] })
|
|
204
202
|
|
|
205
203
|
expect(sidebar.projectGroups.map((group) => group.groupKey)).toEqual(["project-1", "project-2", "project-3"])
|
|
206
204
|
})
|
|
@@ -242,7 +240,7 @@ describe("read models", () => {
|
|
|
242
240
|
lastTurnOutcome: null,
|
|
243
241
|
})
|
|
244
242
|
|
|
245
|
-
const sidebar = deriveSidebarData(state, new Map(), 1_000_000)
|
|
243
|
+
const sidebar = deriveSidebarData(state, new Map(), { nowMs: 1_000_000 })
|
|
246
244
|
|
|
247
245
|
expect(sidebar.projectGroups[0]?.previewChats.map((chat) => chat.chatId)).toEqual(["chat-1"])
|
|
248
246
|
expect(sidebar.projectGroups[0]?.olderChats.map((chat) => chat.chatId)).toEqual(["chat-2"])
|
|
@@ -49,8 +49,12 @@ function getSidebarChatBuckets(chats: SidebarChatRow[], nowMs: number) {
|
|
|
49
49
|
export function deriveSidebarData(
|
|
50
50
|
state: StoreState,
|
|
51
51
|
activeStatuses: Map<string, KannaStatus>,
|
|
52
|
-
|
|
52
|
+
options?: {
|
|
53
|
+
nowMs?: number
|
|
54
|
+
sidebarProjectOrder?: string[]
|
|
55
|
+
}
|
|
53
56
|
): SidebarData {
|
|
57
|
+
const nowMs = options?.nowMs ?? Date.now()
|
|
54
58
|
const chatsByProjectId = new Map<string, ChatRecord[]>()
|
|
55
59
|
for (const chat of state.chatsById.values()) {
|
|
56
60
|
if (chat.deletedAt) continue
|
|
@@ -67,7 +71,7 @@ export function deriveSidebarData(
|
|
|
67
71
|
const unorderedProjects = allProjects
|
|
68
72
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
69
73
|
const projectById = new Map(unorderedProjects.map((project) => [project.id, project]))
|
|
70
|
-
const orderedProjects =
|
|
74
|
+
const orderedProjects = (options?.sidebarProjectOrder ?? [])
|
|
71
75
|
.map((projectId) => projectById.get(projectId))
|
|
72
76
|
.filter((project): project is NonNullable<typeof project> => Boolean(project))
|
|
73
77
|
const orderedProjectIds = new Set(orderedProjects.map((project) => project.id))
|
package/src/server/share.ts
CHANGED
|
@@ -63,42 +63,10 @@ function createNamedTunnel(token: string, localUrl: string) {
|
|
|
63
63
|
return tunnel
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
async function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const cleanup = () => {
|
|
71
|
-
tunnel.off("url", handleUrl)
|
|
72
|
-
tunnel.off("error", handleError)
|
|
73
|
-
tunnel.off("exit", handleExit)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const settle = (callback: () => void) => {
|
|
77
|
-
if (settled) return
|
|
78
|
-
settled = true
|
|
79
|
-
cleanup()
|
|
80
|
-
callback()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const handleUrl = (url: string) => {
|
|
84
|
-
settle(() => resolve(normalizePublicUrl(url)))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const handleError = (error: Error) => {
|
|
88
|
-
settle(() => reject(error))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
92
|
-
settle(() => reject(new Error(`Cloudflare tunnel exited before a public URL was ready (code: ${String(code)}, signal: ${String(signal)})`)))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
tunnel.once("url", handleUrl)
|
|
96
|
-
tunnel.once("error", handleError)
|
|
97
|
-
tunnel.once("exit", handleExit)
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function awaitNamedTunnelReady(tunnel: ShareTunnelProcess) {
|
|
66
|
+
async function awaitTunnelReady(
|
|
67
|
+
tunnel: ShareTunnelProcess,
|
|
68
|
+
{ expectUrl }: { expectUrl: boolean },
|
|
69
|
+
) {
|
|
102
70
|
return await new Promise<string | null>((resolve, reject) => {
|
|
103
71
|
let settled = false
|
|
104
72
|
|
|
@@ -121,6 +89,7 @@ async function awaitNamedTunnelReady(tunnel: ShareTunnelProcess) {
|
|
|
121
89
|
}
|
|
122
90
|
|
|
123
91
|
const handleConnected = () => {
|
|
92
|
+
if (expectUrl) return
|
|
124
93
|
settle(() => resolve(null))
|
|
125
94
|
}
|
|
126
95
|
|
|
@@ -129,11 +98,14 @@ async function awaitNamedTunnelReady(tunnel: ShareTunnelProcess) {
|
|
|
129
98
|
}
|
|
130
99
|
|
|
131
100
|
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
132
|
-
|
|
101
|
+
const readyState = expectUrl ? "a public URL was ready" : "it was ready"
|
|
102
|
+
settle(() => reject(new Error(`Cloudflare tunnel exited before ${readyState} (code: ${String(code)}, signal: ${String(signal)})`)))
|
|
133
103
|
}
|
|
134
104
|
|
|
135
105
|
tunnel.once("url", handleUrl)
|
|
136
|
-
|
|
106
|
+
if (!expectUrl) {
|
|
107
|
+
tunnel.once("connected", handleConnected)
|
|
108
|
+
}
|
|
137
109
|
tunnel.once("error", handleError)
|
|
138
110
|
tunnel.once("exit", handleExit)
|
|
139
111
|
})
|
|
@@ -145,12 +117,11 @@ export async function startShareTunnel(
|
|
|
145
117
|
deps: ShareTunnelDeps = {},
|
|
146
118
|
): Promise<StartedShareTunnel> {
|
|
147
119
|
await ensureCloudflaredInstalled(deps)
|
|
148
|
-
const
|
|
120
|
+
const namedTunnel = isTokenShareMode(shareMode)
|
|
121
|
+
const tunnel = namedTunnel
|
|
149
122
|
? (deps.createNamedTunnel ?? createNamedTunnel)(shareMode.token, localUrl)
|
|
150
123
|
: (deps.createQuickTunnel ?? ((url) => Tunnel.quick(url)))(localUrl)
|
|
151
|
-
const publicUrl =
|
|
152
|
-
? await awaitNamedTunnelReady(tunnel)
|
|
153
|
-
: await awaitQuickTunnelUrl(tunnel)
|
|
124
|
+
const publicUrl = await awaitTunnelReady(tunnel, { expectUrl: !namedTunnel })
|
|
154
125
|
|
|
155
126
|
return {
|
|
156
127
|
publicUrl,
|
|
@@ -863,12 +863,16 @@ describe("ws-router", () => {
|
|
|
863
863
|
})
|
|
864
864
|
|
|
865
865
|
const setSidebarProjectOrderCalls: string[][] = []
|
|
866
|
+
let sidebarProjectOrder: string[] = []
|
|
866
867
|
const router = createWsRouter({
|
|
867
868
|
store: {
|
|
868
869
|
state,
|
|
870
|
+
getSidebarProjectOrder() {
|
|
871
|
+
return [...sidebarProjectOrder]
|
|
872
|
+
},
|
|
869
873
|
async setSidebarProjectOrder(projectIds: string[]) {
|
|
870
874
|
setSidebarProjectOrderCalls.push(projectIds)
|
|
871
|
-
|
|
875
|
+
sidebarProjectOrder = [...projectIds]
|
|
872
876
|
},
|
|
873
877
|
} as never,
|
|
874
878
|
agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
|
package/src/server/ws-router.ts
CHANGED
|
@@ -126,6 +126,12 @@ interface SnapshotComputationCache {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function getSidebarProjectOrder(store: EventStore) {
|
|
130
|
+
return typeof store.getSidebarProjectOrder === "function"
|
|
131
|
+
? store.getSidebarProjectOrder()
|
|
132
|
+
: []
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
130
136
|
const payload = JSON.stringify(message)
|
|
131
137
|
ws.send(payload)
|
|
@@ -300,7 +306,9 @@ export function createWsRouter({
|
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
const startedAt = performance.now()
|
|
303
|
-
const data = deriveSidebarData(store.state, agent.getActiveStatuses()
|
|
309
|
+
const data = deriveSidebarData(store.state, agent.getActiveStatuses(), {
|
|
310
|
+
sidebarProjectOrder: getSidebarProjectOrder(store),
|
|
311
|
+
})
|
|
304
312
|
if (isSendToStartingProfilingEnabled()) {
|
|
305
313
|
const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
|
|
306
314
|
console.log("[kanna/send->starting][server]", JSON.stringify({
|
package/src/shared/dev-ports.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ShareMode } from "./share"
|
|
2
|
-
import { isShareEnabled } from "./share"
|
|
2
|
+
import { assertNoHostOverride, getShareCliFlag, isShareEnabled } from "./share"
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_DEV_CLIENT_PORT = 5174
|
|
5
5
|
|
|
@@ -89,14 +89,12 @@ export function parseDevArgs(args: string[], localHostname: string): DevArgResol
|
|
|
89
89
|
for (let index = 0; index < args.length; index += 1) {
|
|
90
90
|
const arg = args[index]
|
|
91
91
|
if (arg === "--share") {
|
|
92
|
-
|
|
93
|
-
if (sawRemote) throw new Error("--share cannot be used with --remote")
|
|
92
|
+
assertNoHostOverride("--share", sawHost, sawRemote)
|
|
94
93
|
share = "quick"
|
|
95
94
|
continue
|
|
96
95
|
}
|
|
97
96
|
if (arg === "--cloudflared") {
|
|
98
|
-
|
|
99
|
-
if (sawRemote) throw new Error("--cloudflared cannot be used with --remote")
|
|
97
|
+
assertNoHostOverride("--cloudflared", sawHost, sawRemote)
|
|
100
98
|
const next = args[index + 1]
|
|
101
99
|
if (!next || next.startsWith("-")) throw new Error("Missing value for --cloudflared")
|
|
102
100
|
share = { kind: "token", token: next }
|
|
@@ -105,7 +103,7 @@ export function parseDevArgs(args: string[], localHostname: string): DevArgResol
|
|
|
105
103
|
}
|
|
106
104
|
if (arg === "--remote") {
|
|
107
105
|
if (isShareEnabled(share)) {
|
|
108
|
-
throw new Error(
|
|
106
|
+
throw new Error(`${getShareCliFlag(share)} cannot be used with --remote`)
|
|
109
107
|
}
|
|
110
108
|
sawRemote = true
|
|
111
109
|
backendTargetHost = "127.0.0.1"
|
|
@@ -117,7 +115,7 @@ export function parseDevArgs(args: string[], localHostname: string): DevArgResol
|
|
|
117
115
|
const next = args[index + 1]
|
|
118
116
|
if (!next || next.startsWith("-")) continue
|
|
119
117
|
if (isShareEnabled(share)) {
|
|
120
|
-
throw new Error(
|
|
118
|
+
throw new Error(`${getShareCliFlag(share)} cannot be used with --host`)
|
|
121
119
|
}
|
|
122
120
|
sawHost = true
|
|
123
121
|
hosts.add(next)
|
package/src/shared/share.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type ShareMode = false | "quick" | {
|
|
|
3
3
|
token: string
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export type ShareCliFlag = "--share" | "--cloudflared"
|
|
7
|
+
|
|
6
8
|
export function isShareEnabled(share: ShareMode): share is Exclude<ShareMode, false> {
|
|
7
9
|
return share !== false
|
|
8
10
|
}
|
|
@@ -10,3 +12,16 @@ export function isShareEnabled(share: ShareMode): share is Exclude<ShareMode, fa
|
|
|
10
12
|
export function isTokenShareMode(share: ShareMode): share is { kind: "token"; token: string } {
|
|
11
13
|
return typeof share === "object" && share !== null && share.kind === "token"
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function getShareCliFlag(share: Exclude<ShareMode, false>): ShareCliFlag {
|
|
17
|
+
return isTokenShareMode(share) ? "--cloudflared" : "--share"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertNoHostOverride(shareFlag: ShareCliFlag, sawHost: boolean, sawRemote: boolean) {
|
|
21
|
+
if (sawHost) {
|
|
22
|
+
throw new Error(`${shareFlag} cannot be used with --host`)
|
|
23
|
+
}
|
|
24
|
+
if (sawRemote) {
|
|
25
|
+
throw new Error(`${shareFlag} cannot be used with --remote`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
normalizeClaudeModelId,
|
|
4
|
+
normalizeCodexModelId,
|
|
5
|
+
supportsClaudeMaxReasoningEffort,
|
|
6
|
+
} from "./types"
|
|
7
|
+
|
|
8
|
+
describe("shared model normalization", () => {
|
|
9
|
+
test("normalizes Claude aliases via the provider catalog", () => {
|
|
10
|
+
expect(normalizeClaudeModelId("opus")).toBe("claude-opus-4-7")
|
|
11
|
+
expect(normalizeClaudeModelId("sonnet")).toBe("claude-sonnet-4-6")
|
|
12
|
+
expect(normalizeClaudeModelId("haiku")).toBe("claude-haiku-4-5-20251001")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("normalizes legacy Codex aliases via the provider catalog", () => {
|
|
16
|
+
expect(normalizeCodexModelId("gpt-5-codex")).toBe("gpt-5.3-codex")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("uses declarative metadata for Claude max-effort support", () => {
|
|
20
|
+
expect(supportsClaudeMaxReasoningEffort("claude-opus-4-7")).toBe(true)
|
|
21
|
+
expect(supportsClaudeMaxReasoningEffort("opus")).toBe(true)
|
|
22
|
+
expect(supportsClaudeMaxReasoningEffort("claude-sonnet-4-6")).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
})
|