kaizenai 0.2.2 → 0.3.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/dist/client/assets/index-BBs80KD-.js +623 -0
- package/dist/client/assets/index-CkCgyLNq.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent.ts +52 -14
- package/src/server/event-store.ts +119 -18
- package/src/server/events.ts +8 -0
- package/src/server/ws-router.ts +34 -1
- package/src/shared/protocol.ts +5 -1
- package/src/shared/types.ts +12 -0
- package/dist/client/assets/index-Cojjg8ln.js +0 -598
- package/dist/client/assets/index-lSe049cB.css +0 -32
|
@@ -26,6 +26,7 @@ const FEATURE_METADATA_VERSION = 1 as const
|
|
|
26
26
|
|
|
27
27
|
interface PersistedProjectFeature {
|
|
28
28
|
v: typeof FEATURE_METADATA_VERSION
|
|
29
|
+
id: string
|
|
29
30
|
title: string
|
|
30
31
|
description: string
|
|
31
32
|
browserState: FeatureBrowserState
|
|
@@ -81,6 +82,7 @@ export class EventStore {
|
|
|
81
82
|
await this.loadSnapshot()
|
|
82
83
|
await this.replayLogs()
|
|
83
84
|
this.hydrateProjectWorktrees()
|
|
85
|
+
await this.reconcileAllProjectFeatureState()
|
|
84
86
|
if (!(await this.hasLegacyTranscriptData()) && await this.shouldCompact()) {
|
|
85
87
|
await this.compact()
|
|
86
88
|
}
|
|
@@ -99,6 +101,13 @@ export class EventStore {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
private async reconcileAllProjectFeatureState() {
|
|
105
|
+
for (const project of this.state.projectsById.values()) {
|
|
106
|
+
if (project.deletedAt) continue
|
|
107
|
+
await this.reconcileProjectFeatureState(project.id)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
private async ensureFile(filePath: string) {
|
|
103
112
|
const file = Bun.file(filePath)
|
|
104
113
|
if (!(await file.exists())) {
|
|
@@ -334,6 +343,12 @@ export class EventStore {
|
|
|
334
343
|
const feature = this.state.featuresById.get(event.featureId)
|
|
335
344
|
if (!feature) break
|
|
336
345
|
feature.title = event.title
|
|
346
|
+
if (event.directoryRelativePath) {
|
|
347
|
+
feature.directoryRelativePath = event.directoryRelativePath
|
|
348
|
+
}
|
|
349
|
+
if (typeof event.overviewRelativePath === "string") {
|
|
350
|
+
feature.overviewRelativePath = event.overviewRelativePath
|
|
351
|
+
}
|
|
337
352
|
feature.updatedAt = event.timestamp
|
|
338
353
|
break
|
|
339
354
|
}
|
|
@@ -377,6 +392,12 @@ export class EventStore {
|
|
|
377
392
|
}
|
|
378
393
|
break
|
|
379
394
|
}
|
|
395
|
+
case "feature_overview_updated": {
|
|
396
|
+
const feature = this.state.featuresById.get(event.featureId)
|
|
397
|
+
if (!feature) break
|
|
398
|
+
feature.updatedAt = event.timestamp
|
|
399
|
+
break
|
|
400
|
+
}
|
|
380
401
|
case "chat_created": {
|
|
381
402
|
const chat = {
|
|
382
403
|
id: event.chatId,
|
|
@@ -608,7 +629,7 @@ export class EventStore {
|
|
|
608
629
|
await this.append(this.projectsLogPath, event)
|
|
609
630
|
}
|
|
610
631
|
|
|
611
|
-
async createFeature(projectId: string, title: string, description = "") {
|
|
632
|
+
async createFeature(projectId: string, title: string, description = "", generateOverview = true) {
|
|
612
633
|
const project = this.getProject(projectId)
|
|
613
634
|
if (!project) {
|
|
614
635
|
throw new Error("Project not found")
|
|
@@ -621,17 +642,19 @@ export class EventStore {
|
|
|
621
642
|
|
|
622
643
|
const directoryName = this.generateUniqueFeatureDirectoryName(projectId, trimmedTitle)
|
|
623
644
|
const directoryRelativePath = path.posix.join(PROJECT_METADATA_DIR_NAME, directoryName)
|
|
624
|
-
const overviewRelativePath = path.posix.join(directoryRelativePath, "overview.md")
|
|
645
|
+
const overviewRelativePath = generateOverview ? path.posix.join(directoryRelativePath, "overview.md") : ""
|
|
625
646
|
const featureDirPath = path.join(project.localPath, directoryRelativePath)
|
|
626
|
-
const overviewPath = path.join(project.localPath, overviewRelativePath)
|
|
627
647
|
const sortOrder = this.getNextFeatureSortOrder(projectId, "idea")
|
|
628
648
|
|
|
629
649
|
await mkdir(featureDirPath, { recursive: true })
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
650
|
+
if (generateOverview) {
|
|
651
|
+
const overviewPath = path.join(project.localPath, overviewRelativePath)
|
|
652
|
+
await writeFile(overviewPath, this.buildFeatureOverviewContent({
|
|
653
|
+
projectTitle: project.title,
|
|
654
|
+
featureTitle: trimmedTitle,
|
|
655
|
+
description: trimmedDescription,
|
|
656
|
+
}), "utf8")
|
|
657
|
+
}
|
|
635
658
|
|
|
636
659
|
const featureId = crypto.randomUUID()
|
|
637
660
|
const event: FeatureEvent = {
|
|
@@ -658,12 +681,33 @@ export class EventStore {
|
|
|
658
681
|
if (!trimmedTitle) return
|
|
659
682
|
const feature = this.requireFeature(featureId)
|
|
660
683
|
if (feature.title === trimmedTitle) return
|
|
684
|
+
|
|
685
|
+
const project = this.getProject(feature.projectId)
|
|
686
|
+
if (!project) throw new Error("Project not found")
|
|
687
|
+
|
|
688
|
+
const newDirectoryName = this.generateUniqueFeatureDirectoryName(feature.projectId, trimmedTitle)
|
|
689
|
+
const newDirectoryRelativePath = path.posix.join(PROJECT_METADATA_DIR_NAME, newDirectoryName)
|
|
690
|
+
const newOverviewRelativePath = feature.overviewRelativePath
|
|
691
|
+
? path.posix.join(newDirectoryRelativePath, "overview.md")
|
|
692
|
+
: ""
|
|
693
|
+
|
|
694
|
+
const oldDirPath = path.join(project.localPath, feature.directoryRelativePath)
|
|
695
|
+
const newDirPath = path.join(project.localPath, newDirectoryRelativePath)
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
await rename(oldDirPath, newDirPath)
|
|
699
|
+
} catch (error: any) {
|
|
700
|
+
if (error.code !== "ENOENT") throw error
|
|
701
|
+
}
|
|
702
|
+
|
|
661
703
|
const event: FeatureEvent = {
|
|
662
704
|
v: STORE_VERSION,
|
|
663
705
|
type: "feature_renamed",
|
|
664
706
|
timestamp: Date.now(),
|
|
665
707
|
featureId,
|
|
666
708
|
title: trimmedTitle,
|
|
709
|
+
directoryRelativePath: newDirectoryRelativePath,
|
|
710
|
+
overviewRelativePath: newOverviewRelativePath,
|
|
667
711
|
}
|
|
668
712
|
await this.append(this.featuresLogPath, event)
|
|
669
713
|
await this.syncProjectFeatureState(feature.projectId)
|
|
@@ -739,6 +783,57 @@ export class EventStore {
|
|
|
739
783
|
await this.syncProjectFeatureState(feature.projectId)
|
|
740
784
|
}
|
|
741
785
|
|
|
786
|
+
getFeatureOverviewSnapshot(featureId: string) {
|
|
787
|
+
const feature = this.getFeature(featureId)
|
|
788
|
+
if (!feature) return null
|
|
789
|
+
if (!feature.overviewRelativePath) return null
|
|
790
|
+
const project = this.getProject(feature.projectId)
|
|
791
|
+
if (!project) return null
|
|
792
|
+
|
|
793
|
+
const overviewPath = path.join(project.localPath, feature.overviewRelativePath)
|
|
794
|
+
const content = existsSync(overviewPath)
|
|
795
|
+
? readFileSyncImmediate(overviewPath, "utf8")
|
|
796
|
+
: this.buildFeatureOverviewContent({
|
|
797
|
+
projectTitle: project.title,
|
|
798
|
+
featureTitle: feature.title,
|
|
799
|
+
description: feature.description,
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
featureId: feature.id,
|
|
804
|
+
projectId: project.id,
|
|
805
|
+
title: feature.title,
|
|
806
|
+
directoryRelativePath: feature.directoryRelativePath,
|
|
807
|
+
overviewRelativePath: feature.overviewRelativePath,
|
|
808
|
+
localPath: overviewPath,
|
|
809
|
+
content,
|
|
810
|
+
updatedAt: feature.updatedAt,
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async updateFeatureOverview(featureId: string, content: string) {
|
|
815
|
+
const feature = this.requireFeature(featureId)
|
|
816
|
+
if (!feature.overviewRelativePath) {
|
|
817
|
+
throw new Error("Feature overview is disabled for this feature")
|
|
818
|
+
}
|
|
819
|
+
const project = this.getProject(feature.projectId)
|
|
820
|
+
if (!project) {
|
|
821
|
+
throw new Error("Project not found")
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const overviewPath = path.join(project.localPath, feature.overviewRelativePath)
|
|
825
|
+
await mkdir(path.dirname(overviewPath), { recursive: true })
|
|
826
|
+
await writeFile(overviewPath, content, "utf8")
|
|
827
|
+
await this.append(this.featuresLogPath, {
|
|
828
|
+
v: STORE_VERSION,
|
|
829
|
+
type: "feature_overview_updated",
|
|
830
|
+
timestamp: Date.now(),
|
|
831
|
+
featureId,
|
|
832
|
+
})
|
|
833
|
+
await this.syncProjectFeatureState(feature.projectId, true)
|
|
834
|
+
return this.getFeatureOverviewSnapshot(featureId)
|
|
835
|
+
}
|
|
836
|
+
|
|
742
837
|
async hideProject(localPath: string) {
|
|
743
838
|
const identity = resolveProjectRepositoryIdentity(localPath)
|
|
744
839
|
if (this.state.hiddenProjectKeys.has(identity.repoKey)) return
|
|
@@ -945,19 +1040,11 @@ export class EventStore {
|
|
|
945
1040
|
}
|
|
946
1041
|
|
|
947
1042
|
const persistedFeatures = await this.readProjectFeatureMetadata(project.localPath)
|
|
948
|
-
const persistedByDirectory = new Map(
|
|
949
|
-
persistedFeatures.map((feature) => [feature.directoryRelativePath, feature] as const)
|
|
950
|
-
)
|
|
951
1043
|
const existingFeatures = this.listFeaturesByProject(projectId)
|
|
952
1044
|
const existingByDirectory = new Map(
|
|
953
1045
|
existingFeatures.map((feature) => [feature.directoryRelativePath, feature] as const)
|
|
954
1046
|
)
|
|
955
1047
|
|
|
956
|
-
for (const feature of existingFeatures) {
|
|
957
|
-
if (persistedByDirectory.has(feature.directoryRelativePath)) continue
|
|
958
|
-
await this.deleteFeature(feature.id)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
1048
|
const chatsByKey = new Map(
|
|
962
1049
|
this.listChatsByProject(projectId)
|
|
963
1050
|
.map((chat) => {
|
|
@@ -973,7 +1060,7 @@ export class EventStore {
|
|
|
973
1060
|
if (existingFeature) {
|
|
974
1061
|
continue
|
|
975
1062
|
}
|
|
976
|
-
const featureId =
|
|
1063
|
+
const featureId = persistedFeature.id
|
|
977
1064
|
const timestamp = Date.now()
|
|
978
1065
|
const event: FeatureEvent = {
|
|
979
1066
|
v: STORE_VERSION,
|
|
@@ -1006,6 +1093,7 @@ export class EventStore {
|
|
|
1006
1093
|
}
|
|
1007
1094
|
}
|
|
1008
1095
|
|
|
1096
|
+
await this.syncProjectFeatureState(projectId, true)
|
|
1009
1097
|
return importedFeatures
|
|
1010
1098
|
}
|
|
1011
1099
|
|
|
@@ -1305,6 +1393,7 @@ export class EventStore {
|
|
|
1305
1393
|
for (const feature of this.listFeaturesByProject(projectId)) {
|
|
1306
1394
|
const persistedFeature: PersistedProjectFeature = {
|
|
1307
1395
|
v: FEATURE_METADATA_VERSION,
|
|
1396
|
+
id: feature.id,
|
|
1308
1397
|
title: feature.title,
|
|
1309
1398
|
description: feature.description,
|
|
1310
1399
|
browserState: feature.browserState,
|
|
@@ -1319,6 +1408,17 @@ export class EventStore {
|
|
|
1319
1408
|
}
|
|
1320
1409
|
const featureMetadataPath = this.getFeatureMetadataPath(project.localPath, feature.directoryRelativePath)
|
|
1321
1410
|
await mkdir(path.dirname(featureMetadataPath), { recursive: true })
|
|
1411
|
+
if (feature.overviewRelativePath) {
|
|
1412
|
+
const overviewPath = path.join(project.localPath, feature.overviewRelativePath)
|
|
1413
|
+
await mkdir(path.dirname(overviewPath), { recursive: true })
|
|
1414
|
+
if (!existsSync(overviewPath)) {
|
|
1415
|
+
await writeFile(overviewPath, this.buildFeatureOverviewContent({
|
|
1416
|
+
projectTitle: project.title,
|
|
1417
|
+
featureTitle: feature.title,
|
|
1418
|
+
description: feature.description,
|
|
1419
|
+
}), "utf8")
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1322
1422
|
await writeFile(featureMetadataPath, JSON.stringify(persistedFeature, null, 2), "utf8")
|
|
1323
1423
|
}
|
|
1324
1424
|
}
|
|
@@ -1335,7 +1435,7 @@ export class EventStore {
|
|
|
1335
1435
|
if (!raw?.trim()) continue
|
|
1336
1436
|
const parsed = JSON.parse(raw) as Partial<PersistedProjectFeature>
|
|
1337
1437
|
if (
|
|
1338
|
-
parsed.v !== FEATURE_METADATA_VERSION
|
|
1438
|
+
((parsed.v as number | undefined) !== 1 && parsed.v !== FEATURE_METADATA_VERSION)
|
|
1339
1439
|
|| typeof parsed.title !== "string"
|
|
1340
1440
|
|| typeof parsed.description !== "string"
|
|
1341
1441
|
|| typeof parsed.directoryRelativePath !== "string"
|
|
@@ -1348,6 +1448,7 @@ export class EventStore {
|
|
|
1348
1448
|
}
|
|
1349
1449
|
features.push({
|
|
1350
1450
|
v: FEATURE_METADATA_VERSION,
|
|
1451
|
+
id: typeof parsed.id === "string" && parsed.id.trim() ? parsed.id : crypto.randomUUID(),
|
|
1351
1452
|
title: parsed.title,
|
|
1352
1453
|
description: parsed.description,
|
|
1353
1454
|
browserState: (parsed.browserState ?? "OPEN") as FeatureBrowserState,
|
package/src/server/events.ts
CHANGED
|
@@ -109,6 +109,8 @@ export type FeatureEvent =
|
|
|
109
109
|
timestamp: number
|
|
110
110
|
featureId: string
|
|
111
111
|
title: string
|
|
112
|
+
directoryRelativePath?: string
|
|
113
|
+
overviewRelativePath?: string
|
|
112
114
|
}
|
|
113
115
|
| {
|
|
114
116
|
v: 3
|
|
@@ -138,6 +140,12 @@ export type FeatureEvent =
|
|
|
138
140
|
timestamp: number
|
|
139
141
|
featureId: string
|
|
140
142
|
}
|
|
143
|
+
| {
|
|
144
|
+
v: 3
|
|
145
|
+
type: "feature_overview_updated"
|
|
146
|
+
timestamp: number
|
|
147
|
+
featureId: string
|
|
148
|
+
}
|
|
141
149
|
|
|
142
150
|
export type ChatEvent =
|
|
143
151
|
| {
|
package/src/server/ws-router.ts
CHANGED
|
@@ -174,6 +174,18 @@ export function createWsRouter({
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
if (topic.type === "feature-overview") {
|
|
178
|
+
return {
|
|
179
|
+
v: PROTOCOL_VERSION,
|
|
180
|
+
type: "snapshot",
|
|
181
|
+
id,
|
|
182
|
+
snapshot: {
|
|
183
|
+
type: "feature-overview",
|
|
184
|
+
data: store.getFeatureOverviewSnapshot(topic.featureId),
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
return {
|
|
178
190
|
v: PROTOCOL_VERSION,
|
|
179
191
|
type: "snapshot",
|
|
@@ -234,6 +246,15 @@ export function createWsRouter({
|
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
|
|
249
|
+
function pushFeatureOverviewSnapshot(featureId: string) {
|
|
250
|
+
for (const ws of sockets) {
|
|
251
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
252
|
+
if (topic.type !== "feature-overview" || topic.featureId !== featureId) continue
|
|
253
|
+
send(ws, createEnvelope(id, topic))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
237
258
|
function pushTerminalEvent(terminalId: string, event: Extract<ServerEnvelope, { type: "event" }>["event"]) {
|
|
238
259
|
for (const ws of sockets) {
|
|
239
260
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
@@ -523,7 +544,12 @@ export function createWsRouter({
|
|
|
523
544
|
break
|
|
524
545
|
}
|
|
525
546
|
case "feature.create": {
|
|
526
|
-
const feature = await store.createFeature(
|
|
547
|
+
const feature = await store.createFeature(
|
|
548
|
+
command.projectId,
|
|
549
|
+
command.title,
|
|
550
|
+
command.description ?? "",
|
|
551
|
+
command.generateOverview ?? true
|
|
552
|
+
)
|
|
527
553
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { featureId: feature.id } })
|
|
528
554
|
break
|
|
529
555
|
}
|
|
@@ -552,6 +578,13 @@ export function createWsRouter({
|
|
|
552
578
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
553
579
|
break
|
|
554
580
|
}
|
|
581
|
+
case "feature.updateOverview": {
|
|
582
|
+
const snapshot = await store.updateFeatureOverview(command.featureId, command.content)
|
|
583
|
+
pushFeatureOverviewSnapshot(command.featureId)
|
|
584
|
+
broadcastSidebarSnapshots()
|
|
585
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
586
|
+
break
|
|
587
|
+
}
|
|
555
588
|
case "chat.create": {
|
|
556
589
|
const chat = await store.createChat(command.projectId, command.featureId)
|
|
557
590
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
package/src/shared/protocol.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
LocalProjectsSnapshot,
|
|
11
11
|
ModelOptions,
|
|
12
12
|
SidebarData,
|
|
13
|
+
FeatureOverviewSnapshot,
|
|
13
14
|
ThemeSettingsSnapshot,
|
|
14
15
|
UpdateSnapshot,
|
|
15
16
|
} from "./types"
|
|
@@ -29,6 +30,7 @@ export type SubscriptionTopic =
|
|
|
29
30
|
| { type: "theme-settings" }
|
|
30
31
|
| { type: "provider-settings" }
|
|
31
32
|
| { type: "chat"; chatId: string }
|
|
33
|
+
| { type: "feature-overview"; featureId: string }
|
|
32
34
|
| { type: "terminal"; terminalId: string }
|
|
33
35
|
|
|
34
36
|
export interface TerminalSnapshot {
|
|
@@ -75,12 +77,13 @@ export type ClientCommand =
|
|
|
75
77
|
editor?: EditorOpenSettings
|
|
76
78
|
}
|
|
77
79
|
| { type: "chat.create"; projectId: string; featureId?: string }
|
|
78
|
-
| { type: "feature.create"; projectId: string; title: string; description?: string }
|
|
80
|
+
| { type: "feature.create"; projectId: string; title: string; description?: string; generateOverview?: boolean }
|
|
79
81
|
| { type: "feature.rename"; featureId: string; title: string }
|
|
80
82
|
| { type: "feature.setBrowserState"; featureId: string; browserState: FeatureBrowserState }
|
|
81
83
|
| { type: "feature.setStage"; featureId: string; stage: FeatureStage }
|
|
82
84
|
| { type: "feature.reorder"; projectId: string; orderedFeatureIds: string[] }
|
|
83
85
|
| { type: "feature.delete"; featureId: string }
|
|
86
|
+
| { type: "feature.updateOverview"; featureId: string; content: string }
|
|
84
87
|
| { type: "chat.setFeature"; chatId: string; featureId: string | null }
|
|
85
88
|
| { type: "chat.rename"; chatId: string; title: string }
|
|
86
89
|
| { type: "chat.delete"; chatId: string }
|
|
@@ -121,6 +124,7 @@ export type ServerSnapshot =
|
|
|
121
124
|
| { type: "theme-settings"; data: ThemeSettingsSnapshot }
|
|
122
125
|
| { type: "provider-settings"; data: ProviderSettingsSnapshot }
|
|
123
126
|
| { type: "chat"; data: ChatSnapshot | null }
|
|
127
|
+
| { type: "feature-overview"; data: FeatureOverviewSnapshot | null }
|
|
124
128
|
| { type: "terminal"; data: TerminalSnapshot | null }
|
|
125
129
|
|
|
126
130
|
export type ServerEnvelope =
|
package/src/shared/types.ts
CHANGED
|
@@ -467,6 +467,17 @@ export interface SidebarFeatureRow {
|
|
|
467
467
|
chats: SidebarChatRow[]
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
export interface FeatureOverviewSnapshot {
|
|
471
|
+
featureId: string
|
|
472
|
+
projectId: string
|
|
473
|
+
title: string
|
|
474
|
+
directoryRelativePath: string
|
|
475
|
+
overviewRelativePath: string
|
|
476
|
+
localPath: string
|
|
477
|
+
content: string
|
|
478
|
+
updatedAt: number
|
|
479
|
+
}
|
|
480
|
+
|
|
470
481
|
export interface SidebarProjectGroup {
|
|
471
482
|
groupKey: string
|
|
472
483
|
title: string
|
|
@@ -888,6 +899,7 @@ export interface ExitPlanModeToolResult {
|
|
|
888
899
|
clearContext?: boolean
|
|
889
900
|
message?: string
|
|
890
901
|
discarded?: boolean
|
|
902
|
+
attachments?: ChatAttachmentUpload[]
|
|
891
903
|
}
|
|
892
904
|
|
|
893
905
|
export type HydratedAskUserQuestionToolCall =
|