kaizenai 0.1.0
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/LICENSE +22 -0
- package/README.md +246 -0
- package/bin/kaizen +15 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-D-ORCGrq.js +603 -0
- package/dist/client/assets/index-r28mcHqz.css +32 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest-dark.webmanifest +24 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/client/pwa-icon.svg +15 -0
- package/dist/client/pwa-splash.png +0 -0
- package/dist/client/pwa-splash.svg +15 -0
- package/package.json +103 -0
- package/src/server/acp-shared.ts +315 -0
- package/src/server/agent.ts +1120 -0
- package/src/server/attachments.ts +133 -0
- package/src/server/backgrounds.ts +74 -0
- package/src/server/cli-runtime.ts +333 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +68 -0
- package/src/server/codex-app-server-protocol.ts +453 -0
- package/src/server/codex-app-server.ts +1350 -0
- package/src/server/cursor-acp.ts +819 -0
- package/src/server/discovery.ts +322 -0
- package/src/server/event-store.ts +1369 -0
- package/src/server/events.ts +244 -0
- package/src/server/external-open.ts +272 -0
- package/src/server/gemini-acp.ts +844 -0
- package/src/server/gemini-cli.ts +525 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-manager.ts +79 -0
- package/src/server/git-repository.ts +101 -0
- package/src/server/harness-types.ts +20 -0
- package/src/server/keybindings.ts +177 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +112 -0
- package/src/server/process-utils.ts +22 -0
- package/src/server/project-icon.ts +344 -0
- package/src/server/project-metadata.ts +10 -0
- package/src/server/provider-catalog.ts +85 -0
- package/src/server/provider-settings.ts +155 -0
- package/src/server/quick-response.ts +153 -0
- package/src/server/read-models.ts +275 -0
- package/src/server/recovery.ts +507 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +244 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/theme-settings.ts +179 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/usage/base-provider-usage.ts +57 -0
- package/src/server/usage/claude-usage.ts +558 -0
- package/src/server/usage/codex-usage.ts +144 -0
- package/src/server/usage/cursor-browser.ts +120 -0
- package/src/server/usage/cursor-cookies.ts +390 -0
- package/src/server/usage/cursor-usage.ts +490 -0
- package/src/server/usage/gemini-usage.ts +24 -0
- package/src/server/usage/provider-usage.ts +61 -0
- package/src/server/usage/test-helpers.ts +9 -0
- package/src/server/usage/types.ts +54 -0
- package/src/server/usage/utils.ts +325 -0
- package/src/server/ws-router.ts +717 -0
- package/src/shared/branding.ts +83 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +152 -0
- package/src/shared/tools.ts +251 -0
- package/src/shared/types.ts +1028 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import { CodexAppServerManager } from "./codex-app-server"
|
|
3
|
+
|
|
4
|
+
const CLAUDE_STRUCTURED_TIMEOUT_MS = 5_000
|
|
5
|
+
|
|
6
|
+
type JsonSchema = {
|
|
7
|
+
type: "object"
|
|
8
|
+
properties: Record<string, unknown>
|
|
9
|
+
required?: readonly string[]
|
|
10
|
+
additionalProperties?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StructuredQuickResponseArgs<T> {
|
|
14
|
+
cwd: string
|
|
15
|
+
task: string
|
|
16
|
+
prompt: string
|
|
17
|
+
schema: JsonSchema
|
|
18
|
+
parse: (value: unknown) => T | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface QuickResponseAdapterArgs {
|
|
22
|
+
codexManager?: CodexAppServerManager
|
|
23
|
+
runClaudeStructured?: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
|
|
24
|
+
runCodexStructured?: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseJsonText(value: string): unknown | null {
|
|
28
|
+
const trimmed = value.trim()
|
|
29
|
+
if (!trimmed) return null
|
|
30
|
+
|
|
31
|
+
const candidates = [trimmed]
|
|
32
|
+
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)
|
|
33
|
+
if (fencedMatch?.[1]) {
|
|
34
|
+
candidates.unshift(fencedMatch[1].trim())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(candidate)
|
|
40
|
+
} catch {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknown>, "parse">): Promise<unknown | null> {
|
|
49
|
+
const q = query({
|
|
50
|
+
prompt: args.prompt,
|
|
51
|
+
options: {
|
|
52
|
+
model: "haiku",
|
|
53
|
+
tools: [],
|
|
54
|
+
systemPrompt: "",
|
|
55
|
+
effort: "low",
|
|
56
|
+
permissionMode: "bypassPermissions",
|
|
57
|
+
outputFormat: {
|
|
58
|
+
type: "json_schema",
|
|
59
|
+
schema: args.schema,
|
|
60
|
+
},
|
|
61
|
+
env: { ...process.env },
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await Promise.race<unknown | null>([
|
|
67
|
+
(async () => {
|
|
68
|
+
for await (const message of q) {
|
|
69
|
+
if ("result" in message) {
|
|
70
|
+
return (message as Record<string, unknown>).structured_output ?? null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
})(),
|
|
75
|
+
new Promise<null>((_, reject) => {
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
reject(new Error(`Claude structured response timed out after ${CLAUDE_STRUCTURED_TIMEOUT_MS}ms`))
|
|
78
|
+
}, CLAUDE_STRUCTURED_TIMEOUT_MS)
|
|
79
|
+
}),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return null
|
|
85
|
+
} finally {
|
|
86
|
+
try {
|
|
87
|
+
q.close()
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore close failures on timed-out or failed quick responses.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runCodexStructured(
|
|
95
|
+
codexManager: CodexAppServerManager,
|
|
96
|
+
args: Omit<StructuredQuickResponseArgs<unknown>, "parse">
|
|
97
|
+
): Promise<unknown | null> {
|
|
98
|
+
const response = await codexManager.generateStructured({
|
|
99
|
+
cwd: args.cwd,
|
|
100
|
+
prompt: `${args.prompt}\n\nReturn JSON only that matches this schema:\n${JSON.stringify(args.schema, null, 2)}`,
|
|
101
|
+
})
|
|
102
|
+
if (typeof response !== "string") return null
|
|
103
|
+
return parseJsonText(response)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class QuickResponseAdapter {
|
|
107
|
+
private readonly codexManager: CodexAppServerManager
|
|
108
|
+
private readonly runClaudeStructured: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
|
|
109
|
+
private readonly runCodexStructured: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
|
|
110
|
+
|
|
111
|
+
constructor(args: QuickResponseAdapterArgs = {}) {
|
|
112
|
+
this.codexManager = args.codexManager ?? new CodexAppServerManager()
|
|
113
|
+
this.runClaudeStructured = args.runClaudeStructured ?? runClaudeStructured
|
|
114
|
+
this.runCodexStructured = args.runCodexStructured ?? ((structuredArgs) =>
|
|
115
|
+
runCodexStructured(this.codexManager, structuredArgs))
|
|
116
|
+
}
|
|
117
|
+
async generateStructured<T>(args: StructuredQuickResponseArgs<T>): Promise<T | null> {
|
|
118
|
+
const request = {
|
|
119
|
+
cwd: args.cwd,
|
|
120
|
+
task: args.task,
|
|
121
|
+
prompt: args.prompt,
|
|
122
|
+
schema: args.schema,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const claudeResult = await this.tryProvider("claude", args.task, args.parse, () => this.runClaudeStructured(request))
|
|
126
|
+
if (claudeResult !== null) return claudeResult
|
|
127
|
+
|
|
128
|
+
return await this.tryProvider("codex", args.task, args.parse, () => this.runCodexStructured(request))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async tryProvider<T>(
|
|
132
|
+
provider: "claude" | "codex",
|
|
133
|
+
task: string,
|
|
134
|
+
parse: (value: unknown) => T | null,
|
|
135
|
+
run: () => Promise<unknown | null>
|
|
136
|
+
): Promise<T | null> {
|
|
137
|
+
try {
|
|
138
|
+
const result = await run()
|
|
139
|
+
if (result === null) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parsed = parse(result)
|
|
144
|
+
if (parsed === null) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parsed
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import type {
|
|
5
|
+
ChatPendingToolSnapshot,
|
|
6
|
+
ChatRuntime,
|
|
7
|
+
ChatSnapshot,
|
|
8
|
+
ChatUsageSnapshot,
|
|
9
|
+
DirectoryBrowserSnapshot,
|
|
10
|
+
KaizenStatus,
|
|
11
|
+
LocalProjectsSnapshot,
|
|
12
|
+
ProviderSettingsSnapshot,
|
|
13
|
+
ProviderUsageMap,
|
|
14
|
+
SuggestedProjectFolder,
|
|
15
|
+
SidebarChatRow,
|
|
16
|
+
SidebarData,
|
|
17
|
+
SidebarFeatureRow,
|
|
18
|
+
SidebarProjectGroup,
|
|
19
|
+
} from "../shared/types"
|
|
20
|
+
import type { ChatRecord, StoreState } from "./events"
|
|
21
|
+
import { getDefaultDirectoryRoot, resolveLocalPath } from "./paths"
|
|
22
|
+
import { getProviderInactiveMessage, getSelectableProviders } from "../shared/types"
|
|
23
|
+
import { resolveProjectIconDataUrl } from "./project-icon"
|
|
24
|
+
|
|
25
|
+
function deriveLastChatModel(chatId: string, getMessages: (chatId: string) => ChatSnapshot["messages"]): string | null {
|
|
26
|
+
const entries = getMessages(chatId)
|
|
27
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
28
|
+
const entry = entries[index]
|
|
29
|
+
if (entry.kind === "system_init") {
|
|
30
|
+
return entry.model
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function deriveStatus(chat: ChatRecord, activeStatus?: KaizenStatus): KaizenStatus {
|
|
37
|
+
if (activeStatus) return activeStatus
|
|
38
|
+
if (chat.lastTurnOutcome === "failed") return "failed"
|
|
39
|
+
return "idle"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deriveSidebarData(
|
|
43
|
+
state: StoreState,
|
|
44
|
+
activeStatuses: Map<string, KaizenStatus>,
|
|
45
|
+
providerUsage?: ProviderUsageMap
|
|
46
|
+
): SidebarData {
|
|
47
|
+
const iconDataUrlByPath = new Map<string, string | null>()
|
|
48
|
+
const getProjectIconDataUrl = (localPath: string) => {
|
|
49
|
+
const normalizedPath = resolveLocalPath(localPath)
|
|
50
|
+
if (!iconDataUrlByPath.has(normalizedPath)) {
|
|
51
|
+
iconDataUrlByPath.set(normalizedPath, resolveProjectIconDataUrl(normalizedPath))
|
|
52
|
+
}
|
|
53
|
+
return iconDataUrlByPath.get(normalizedPath) ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const projects = [...state.projectsById.values()]
|
|
57
|
+
.filter((project) => !project.deletedAt && !state.hiddenProjectKeys.has(project.repoKey))
|
|
58
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
59
|
+
|
|
60
|
+
const projectGroups: SidebarProjectGroup[] = projects.map((project) => {
|
|
61
|
+
const chats: SidebarChatRow[] = [...state.chatsById.values()]
|
|
62
|
+
.filter((chat) => chat.projectId === project.id && !chat.deletedAt)
|
|
63
|
+
.sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
|
|
64
|
+
.map((chat) => ({
|
|
65
|
+
_id: chat.id,
|
|
66
|
+
_creationTime: chat.createdAt,
|
|
67
|
+
chatId: chat.id,
|
|
68
|
+
title: chat.title,
|
|
69
|
+
status: deriveStatus(chat, activeStatuses.get(chat.id)),
|
|
70
|
+
localPath: project.localPath,
|
|
71
|
+
provider: chat.provider,
|
|
72
|
+
lastMessageAt: chat.lastMessageAt,
|
|
73
|
+
hasAutomation: false,
|
|
74
|
+
featureId: chat.featureId ?? null,
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
const features: SidebarFeatureRow[] = [...state.featuresById.values()]
|
|
78
|
+
.filter((feature) => feature.projectId === project.id && !feature.deletedAt)
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
const aDone = a.stage === "done" ? 1 : 0
|
|
81
|
+
const bDone = b.stage === "done" ? 1 : 0
|
|
82
|
+
if (aDone !== bDone) return aDone - bDone
|
|
83
|
+
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
|
|
84
|
+
return b.updatedAt - a.updatedAt
|
|
85
|
+
})
|
|
86
|
+
.map((feature) => ({
|
|
87
|
+
featureId: feature.id,
|
|
88
|
+
title: feature.title,
|
|
89
|
+
description: feature.description,
|
|
90
|
+
browserState: feature.browserState,
|
|
91
|
+
stage: feature.stage,
|
|
92
|
+
sortOrder: feature.sortOrder,
|
|
93
|
+
directoryRelativePath: feature.directoryRelativePath,
|
|
94
|
+
overviewRelativePath: feature.overviewRelativePath,
|
|
95
|
+
updatedAt: feature.updatedAt,
|
|
96
|
+
chats: chats.filter((chat) => chat.featureId === feature.id),
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
groupKey: project.id,
|
|
101
|
+
title: project.title,
|
|
102
|
+
localPath: project.localPath,
|
|
103
|
+
iconDataUrl: getProjectIconDataUrl(project.localPath),
|
|
104
|
+
browserState: project.browserState,
|
|
105
|
+
generalChatsBrowserState: project.generalChatsBrowserState,
|
|
106
|
+
features,
|
|
107
|
+
generalChats: chats.filter((chat) => !chat.featureId),
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return { projectGroups, providerUsage }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function deriveLocalProjectsSnapshot(
|
|
115
|
+
state: StoreState,
|
|
116
|
+
discoveredProjects: Array<{ repoKey: string; localPath: string; worktreePaths?: string[]; title: string; modifiedAt: number }>,
|
|
117
|
+
machineName: string
|
|
118
|
+
): LocalProjectsSnapshot {
|
|
119
|
+
const projects = new Map<string, LocalProjectsSnapshot["projects"][number]>()
|
|
120
|
+
const iconDataUrlByPath = new Map<string, string | null>()
|
|
121
|
+
const getProjectIconDataUrl = (localPath: string) => {
|
|
122
|
+
const normalizedPath = resolveLocalPath(localPath)
|
|
123
|
+
if (!iconDataUrlByPath.has(normalizedPath)) {
|
|
124
|
+
iconDataUrlByPath.set(normalizedPath, resolveProjectIconDataUrl(normalizedPath))
|
|
125
|
+
}
|
|
126
|
+
return iconDataUrlByPath.get(normalizedPath) ?? null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const project of discoveredProjects) {
|
|
130
|
+
const normalizedPath = resolveLocalPath(project.localPath)
|
|
131
|
+
projects.set(project.repoKey, {
|
|
132
|
+
localPath: normalizedPath,
|
|
133
|
+
title: project.title,
|
|
134
|
+
iconDataUrl: getProjectIconDataUrl(normalizedPath),
|
|
135
|
+
source: "discovered",
|
|
136
|
+
lastOpenedAt: project.modifiedAt,
|
|
137
|
+
chatCount: 0,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const project of [...state.projectsById.values()].filter((entry) => !entry.deletedAt && !state.hiddenProjectKeys.has(entry.repoKey))) {
|
|
142
|
+
const chats = [...state.chatsById.values()].filter((chat) => chat.projectId === project.id && !chat.deletedAt)
|
|
143
|
+
const lastOpenedAt = chats.reduce(
|
|
144
|
+
(latest, chat) => Math.max(latest, chat.lastMessageAt ?? chat.updatedAt ?? 0),
|
|
145
|
+
project.updatedAt
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
projects.set(project.repoKey, {
|
|
149
|
+
localPath: project.localPath,
|
|
150
|
+
title: project.title,
|
|
151
|
+
iconDataUrl: getProjectIconDataUrl(project.localPath),
|
|
152
|
+
source: "saved",
|
|
153
|
+
lastOpenedAt,
|
|
154
|
+
chatCount: chats.length,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const suggestedFolders = deriveSuggestedProjectFolders(state, discoveredProjects)
|
|
159
|
+
const rootDirectory = deriveRootDirectorySnapshot()
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
machine: {
|
|
163
|
+
id: "local",
|
|
164
|
+
displayName: machineName,
|
|
165
|
+
},
|
|
166
|
+
projects: [...projects.values()].sort((a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0)),
|
|
167
|
+
suggestedFolders,
|
|
168
|
+
rootDirectory,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deriveRootDirectorySnapshot(): DirectoryBrowserSnapshot | null {
|
|
173
|
+
const currentPath = getDefaultDirectoryRoot()
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const entries = readdirSync(currentPath, { withFileTypes: true })
|
|
177
|
+
.filter((entry) => entry.isDirectory())
|
|
178
|
+
.map((entry) => ({
|
|
179
|
+
name: entry.name,
|
|
180
|
+
localPath: path.join(currentPath, entry.name),
|
|
181
|
+
}))
|
|
182
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
currentPath,
|
|
186
|
+
parentPath: path.dirname(currentPath) === currentPath ? null : path.dirname(currentPath),
|
|
187
|
+
roots: [{ name: currentPath, localPath: currentPath }],
|
|
188
|
+
entries,
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function deriveSuggestedProjectFolders(
|
|
196
|
+
state: StoreState,
|
|
197
|
+
discoveredProjects: Array<{ localPath: string }>
|
|
198
|
+
): SuggestedProjectFolder[] {
|
|
199
|
+
const candidates = new Map<string, string>()
|
|
200
|
+
const home = homedir()
|
|
201
|
+
|
|
202
|
+
const addCandidate = (label: string, candidatePath: string | null | undefined) => {
|
|
203
|
+
if (!candidatePath) return
|
|
204
|
+
const resolvedPath = resolveLocalPath(candidatePath)
|
|
205
|
+
if (candidates.has(resolvedPath)) return
|
|
206
|
+
if (!existsSync(resolvedPath)) return
|
|
207
|
+
try {
|
|
208
|
+
if (!statSync(resolvedPath).isDirectory()) return
|
|
209
|
+
} catch {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
candidates.set(resolvedPath, label)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
addCandidate("Home", home)
|
|
216
|
+
addCandidate("Documents", path.join(home, "Documents"))
|
|
217
|
+
addCandidate("Projects", path.join(home, "projects"))
|
|
218
|
+
addCandidate("Projects", path.join(home, "Projects"))
|
|
219
|
+
addCandidate("Downloads", path.join(home, "Downloads"))
|
|
220
|
+
addCandidate("Desktop", path.join(home, "Desktop"))
|
|
221
|
+
addCandidate("Current Project", process.cwd())
|
|
222
|
+
addCandidate("Current Project Parent", path.dirname(process.cwd()))
|
|
223
|
+
|
|
224
|
+
for (const project of discoveredProjects) {
|
|
225
|
+
addCandidate(`Nearby: ${path.basename(project.localPath) || project.localPath}`, project.localPath)
|
|
226
|
+
addCandidate(`Parent: ${path.basename(path.dirname(project.localPath)) || path.dirname(project.localPath)}`, path.dirname(project.localPath))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const project of state.projectsById.values()) {
|
|
230
|
+
if (project.deletedAt) continue
|
|
231
|
+
addCandidate(`Saved: ${project.title}`, project.localPath)
|
|
232
|
+
addCandidate(`Parent: ${path.basename(path.dirname(project.localPath)) || path.dirname(project.localPath)}`, path.dirname(project.localPath))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return [...candidates.entries()]
|
|
236
|
+
.map(([localPath, label]) => ({ localPath, label }))
|
|
237
|
+
.slice(0, 12)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function deriveChatSnapshot(
|
|
241
|
+
state: StoreState,
|
|
242
|
+
activeStatuses: Map<string, KaizenStatus>,
|
|
243
|
+
chatId: string,
|
|
244
|
+
getMessages: (chatId: string) => ChatSnapshot["messages"],
|
|
245
|
+
pendingTool: ChatPendingToolSnapshot | null = null,
|
|
246
|
+
usage: ChatUsageSnapshot | null = null,
|
|
247
|
+
providerSettings?: ProviderSettingsSnapshot["settings"]
|
|
248
|
+
): ChatSnapshot | null {
|
|
249
|
+
const chat = state.chatsById.get(chatId)
|
|
250
|
+
if (!chat || chat.deletedAt) return null
|
|
251
|
+
const project = state.projectsById.get(chat.projectId)
|
|
252
|
+
if (!project || project.deletedAt) return null
|
|
253
|
+
|
|
254
|
+
const runtime: ChatRuntime = {
|
|
255
|
+
chatId: chat.id,
|
|
256
|
+
projectId: project.id,
|
|
257
|
+
localPath: project.localPath,
|
|
258
|
+
title: chat.title,
|
|
259
|
+
status: deriveStatus(chat, activeStatuses.get(chat.id)),
|
|
260
|
+
provider: chat.provider,
|
|
261
|
+
model: deriveLastChatModel(chat.id, getMessages),
|
|
262
|
+
planMode: chat.planMode,
|
|
263
|
+
sessionToken: chat.sessionToken,
|
|
264
|
+
pendingTool,
|
|
265
|
+
inactiveProviderMessage: chat.provider ? getProviderInactiveMessage(chat.provider, providerSettings) : null,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
runtime,
|
|
270
|
+
messages: getMessages(chat.id),
|
|
271
|
+
usage,
|
|
272
|
+
availableProviders: getSelectableProviders(providerSettings),
|
|
273
|
+
providerSettings,
|
|
274
|
+
}
|
|
275
|
+
}
|