@jkwd/inbase 0.1.10 → 0.1.12

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.
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Canvas } from '@react-three/fiber'
3
- import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistSessionBlueprint, persistSessionFocus } from './agentIntent'
3
+ import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistSessionBlueprint, persistSessionFocus, setupVisualizerSession } from './agentIntent'
4
+ import { emptyBranchChanges, fetchBranchChanges } from './branchChanges'
4
5
  import { fetchCodebase, updateCodebase } from './codebase'
5
6
  import {
6
7
  layoutWorld,
@@ -13,9 +14,14 @@ import {
13
14
  } from './layout'
14
15
  import { World } from './scene/World'
15
16
  import { HUD } from './ui/HUD'
17
+ import {
18
+ MapContextMenu,
19
+ type MapContextMenuState,
20
+ } from './ui/MapContextMenu'
16
21
  import {
17
22
  fetchUserContext,
18
23
  persistFollowLook,
24
+ persistShowBranchChanges,
19
25
  persistUserContext,
20
26
  } from './userContext'
21
27
  import {
@@ -34,6 +40,7 @@ import {
34
40
  } from './userCreated'
35
41
  import {
36
42
  isPatchPreview,
43
+ llmIsMakingChanges,
37
44
  type AgentIntent,
38
45
  type AimedRelation,
39
46
  type CodebaseGraph,
@@ -53,7 +60,12 @@ function intentSignature(intent: AgentIntent) {
53
60
  phase: intent.phase,
54
61
  stalledWait: intent.stalledWait,
55
62
  llmIdle: intent.llmIdle,
63
+ awaitingAttach: intent.awaitingAttach,
64
+ listening: intent.listening,
65
+ lastAck: intent.lastAck,
56
66
  sessionId: intent.sessionId,
67
+ name: intent.name,
68
+ feature: intent.feature,
57
69
  creationMode: intent.creationMode,
58
70
  diffId: intent.diffId,
59
71
  chain: intent.chain,
@@ -76,8 +88,15 @@ export default function App() {
76
88
  const [updatingModel, setUpdatingModel] = useState(false)
77
89
  const updatingModelRef = useRef(false)
78
90
 
91
+ const graphSig = useRef<string | null>(null)
79
92
  const applyGraph = useCallback((next: CodebaseGraph | null, failed: string) => {
80
93
  if (next) {
94
+ const signature = JSON.stringify(next)
95
+ if (signature === graphSig.current) {
96
+ setLoadError(null)
97
+ return true
98
+ }
99
+ graphSig.current = signature
81
100
  setGraph(next)
82
101
  setLoadError(null)
83
102
  return true
@@ -137,13 +156,27 @@ function Explorer({
137
156
  }) {
138
157
  const [intents, setIntents] = useState<AgentIntent[]>([])
139
158
  const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null)
159
+ const [wantBranchChanges, setWantBranchChanges] = useState(false)
160
+ const [branchChanges, setBranchChanges] = useState(emptyBranchChanges)
140
161
  const intent =
141
162
  intents.find((item) => item.sessionId === focusedSessionId) ??
142
163
  intents[0] ??
143
164
  emptyIntent
144
165
  const canPlace = Boolean(intent.sessionId && intent.creationMode)
145
- const previewing = intent.preview || isPatchPreview(intent.status)
146
- const plannedCreates = previewing ? intent.creates : []
166
+ const llmBusy = intents.some(llmIsMakingChanges)
167
+ const llmPreviewing = intent.preview || isPatchPreview(intent.status)
168
+ const showingBranchChanges =
169
+ wantBranchChanges &&
170
+ !llmBusy &&
171
+ !llmPreviewing &&
172
+ branchChanges.available
173
+ const changeSet = llmPreviewing
174
+ ? intent
175
+ : showingBranchChanges
176
+ ? branchChanges
177
+ : emptyIntent
178
+ const previewing = llmPreviewing || showingBranchChanges
179
+ const plannedCreates = previewing ? changeSet.creates : []
147
180
  const [userBlocks, setUserBlocks] = useState<UserCreatedBlock[]>([])
148
181
  const [userIslands, setUserIslands] = useState<UserCreatedIsland[]>([])
149
182
  const [blueprintFunctions, setBlueprintFunctions] = useState<
@@ -163,15 +196,15 @@ function Explorer({
163
196
  return withPreviewGraph(
164
197
  graph,
165
198
  plannedCreates,
166
- intent.createLines ?? {},
167
- intent.createFolders ?? [],
168
- intent.imports ?? [],
199
+ changeSet.createLines ?? {},
200
+ changeSet.createFolders ?? [],
201
+ changeSet.imports ?? [],
169
202
  )
170
203
  }, [
204
+ changeSet.createFolders,
205
+ changeSet.createLines,
206
+ changeSet.imports,
171
207
  graph,
172
- intent.createFolders,
173
- intent.createLines,
174
- intent.imports,
175
208
  plannedCreates,
176
209
  previewing,
177
210
  ])
@@ -193,15 +226,23 @@ function Explorer({
193
226
  ],
194
227
  )
195
228
  const layout = useMemo(() => {
196
- const world = layoutWorld(previewGraph)
197
- if (previewing) markCreatedFolders(world, intent.createFolders ?? [])
229
+ const world = layoutWorld(
230
+ withUserCreatedGraph(previewGraph, [], userIslands),
231
+ )
232
+ if (previewing) markCreatedFolders(world, changeSet.createFolders ?? [])
198
233
  return withUserCreatedLayout(world, userBlocks, userIslands)
199
- }, [intent.createFolders, previewGraph, previewing, userBlocks, userIslands])
234
+ }, [
235
+ changeSet.createFolders,
236
+ previewGraph,
237
+ previewing,
238
+ userBlocks,
239
+ userIslands,
240
+ ])
200
241
  const changeFileIds = useMemo(() => {
201
242
  const ids = new Set<string>()
202
- for (const id of intent.files) ids.add(id)
203
- for (const id of intent.creates) ids.add(id)
204
- for (const id of intent.deletes) ids.add(id)
243
+ for (const id of changeSet.files) ids.add(id)
244
+ for (const id of changeSet.creates) ids.add(id)
245
+ for (const id of changeSet.deletes) ids.add(id)
205
246
  for (const block of userBlocks) ids.add(block.id)
206
247
  for (const item of blueprintFunctions) ids.add(item.file)
207
248
  for (const item of blueprintVariables) ids.add(item.file)
@@ -211,20 +252,20 @@ function Explorer({
211
252
  blueprintFunctions,
212
253
  blueprintImports,
213
254
  blueprintVariables,
214
- intent.creates,
215
- intent.deletes,
216
- intent.files,
255
+ changeSet.creates,
256
+ changeSet.deletes,
257
+ changeSet.files,
217
258
  userBlocks,
218
259
  ])
219
260
  const changeFolderPaths = useMemo(() => {
220
261
  const paths = new Set<string>()
221
- for (const path of intent.createFolders ?? []) paths.add(path)
262
+ for (const path of changeSet.createFolders ?? []) paths.add(path)
222
263
  for (const island of userIslands) {
223
264
  if (island.path) paths.add(island.path)
224
265
  else if (island.id) paths.add(island.id)
225
266
  }
226
267
  return [...paths]
227
- }, [intent.createFolders, userIslands])
268
+ }, [changeSet.createFolders, userIslands])
228
269
  const hasChangeSet =
229
270
  changeFileIds.length > 0 || changeFolderPaths.length > 0
230
271
  const changePathGraph = useMemo(() => {
@@ -239,14 +280,14 @@ function Explorer({
239
280
  if (!hasChangeSet) return layout
240
281
  const world = layoutWorld(changePathGraph)
241
282
  markCreatedFolders(world, [
242
- ...(intent.createFolders ?? []),
283
+ ...(changeSet.createFolders ?? []),
243
284
  ...userIslands.map((island) => island.path || island.id),
244
285
  ])
245
286
  return world
246
287
  }, [
247
288
  changePathGraph,
289
+ changeSet.createFolders,
248
290
  hasChangeSet,
249
- intent.createFolders,
250
291
  layout,
251
292
  userIslands,
252
293
  ])
@@ -259,6 +300,7 @@ function Explorer({
259
300
  const [selectedId, setSelectedId] = useState<string | null>(null)
260
301
  const [selectedTick, setSelectedTick] = useState(0)
261
302
  const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
303
+ const [mapMenu, setMapMenu] = useState<MapContextMenuState | null>(null)
262
304
  const [aimedRelation, setAimedRelation] = useState<AimedRelation | null>(null)
263
305
  const [aimedFileId, setAimedFileId] = useState<string | null>(null)
264
306
  const [inspectTick, setInspectTick] = useState(0)
@@ -301,6 +343,14 @@ function Explorer({
301
343
  persistSessionFocus(sessionId)
302
344
  }, [])
303
345
 
346
+ const setupLlmSession = useCallback(async () => {
347
+ const next = await setupVisualizerSession()
348
+ lastIntentSig.current = null
349
+ applyIntent(next, next.sessionId ?? undefined)
350
+ if (next.sessionId) setFocusedSessionId(next.sessionId)
351
+ return next
352
+ }, [applyIntent])
353
+
304
354
  const rememberWalk = useCallback((x: number, z: number) => {
305
355
  walkPos.current = [x, z]
306
356
  }, [])
@@ -418,7 +468,6 @@ function Explorer({
418
468
  lastIntentSig.current = null
419
469
  applyIntent(next, sessionId)
420
470
  if (
421
- action === 'invoke' ||
422
471
  action === 'continue' ||
423
472
  action === 'stop' ||
424
473
  action === 'set_step_by_step'
@@ -509,6 +558,9 @@ function Explorer({
509
558
  if (typeof context?.followLook === 'boolean') {
510
559
  setFollowLook(context.followLook)
511
560
  }
561
+ if (typeof context?.showBranchChanges === 'boolean') {
562
+ setWantBranchChanges(context.showBranchChanges)
563
+ }
512
564
  })
513
565
  return () => {
514
566
  cancelled = true
@@ -680,12 +732,14 @@ function Explorer({
680
732
  const placeIsland = useCallback(
681
733
  (parent: string) => {
682
734
  if (!canPlace) return
735
+ let placedId: string | null = null
683
736
  setUserIslands((current) => {
684
737
  if (current.some((island) => island.naming)) return current
738
+ placedId = `draft:${Date.now()}`
685
739
  return [
686
740
  ...current,
687
741
  {
688
- id: `draft:${Date.now()}`,
742
+ id: placedId,
689
743
  name: '',
690
744
  path: '',
691
745
  parent,
@@ -693,6 +747,10 @@ function Explorer({
693
747
  },
694
748
  ]
695
749
  })
750
+ if (placedId) {
751
+ setSelectedFolder(placedId)
752
+ setSelectedId(null)
753
+ }
696
754
  document.exitPointerLock()
697
755
  },
698
756
  [canPlace],
@@ -727,14 +785,23 @@ function Explorer({
727
785
  )
728
786
  setUserIslands(next)
729
787
  persistBlueprint(userBlocks, next)
788
+ setSelectedFolder(resolved.path)
789
+ setSelectedId(null)
730
790
  return true
731
791
  },
732
792
  [graph.folders, persistBlueprint, userBlocks, userIslands],
733
793
  )
734
794
 
735
- const cancelIslandName = useCallback((id: string) => {
736
- setUserIslands((current) => current.filter((island) => island.id !== id))
737
- }, [])
795
+ const cancelIslandName = useCallback(
796
+ (id: string) => {
797
+ const draft = userIslands.find((island) => island.id === id)
798
+ setUserIslands((current) => current.filter((island) => island.id !== id))
799
+ setSelectedFolder((selected) =>
800
+ selected === id ? draft?.parent ?? null : selected,
801
+ )
802
+ },
803
+ [userIslands],
804
+ )
738
805
 
739
806
  const deleteSelectedCreatedBlock = useCallback(() => {
740
807
  if (!canPlace || !selectedId) return false
@@ -974,6 +1041,10 @@ function Explorer({
974
1041
  return () => window.removeEventListener('keydown', onKey)
975
1042
  }, [cancelBlockName, cancelIslandName, namingId, namingIslandId])
976
1043
 
1044
+ useEffect(() => {
1045
+ if (mode !== 'map' || naming) setMapMenu(null)
1046
+ }, [mode, naming])
1047
+
977
1048
  useEffect(() => {
978
1049
  const onKey = (event: KeyboardEvent) => {
979
1050
  if (event.repeat || event.code !== 'Backspace') return
@@ -1002,6 +1073,18 @@ function Explorer({
1002
1073
  })
1003
1074
  }, [])
1004
1075
 
1076
+ const canToggleBranchChanges = !llmBusy && (wantBranchChanges || branchChanges.available)
1077
+
1078
+ const toggleShowBranchChanges = useCallback(() => {
1079
+ if (llmBusy) return
1080
+ if (!wantBranchChanges && !branchChanges.available) return
1081
+ setWantBranchChanges((current) => {
1082
+ const next = !current
1083
+ persistShowBranchChanges(next)
1084
+ return next
1085
+ })
1086
+ }, [branchChanges.available, llmBusy, wantBranchChanges])
1087
+
1005
1088
  const toggleImportedBy = useCallback(() => {
1006
1089
  setImportedBy((current) => !current)
1007
1090
  }, [])
@@ -1031,6 +1114,48 @@ function Explorer({
1031
1114
  return () => window.removeEventListener('keydown', onKey)
1032
1115
  }, [toggleImportedBy])
1033
1116
 
1117
+ useEffect(() => {
1118
+ const onKey = (event: KeyboardEvent) => {
1119
+ if (event.repeat || event.code !== 'KeyG') return
1120
+ const target = event.target
1121
+ if (
1122
+ target instanceof HTMLElement &&
1123
+ (target.tagName === 'TEXTAREA' ||
1124
+ target.tagName === 'INPUT' ||
1125
+ target.tagName === 'SELECT' ||
1126
+ target.isContentEditable)
1127
+ ) {
1128
+ return
1129
+ }
1130
+ if (!canToggleBranchChanges) return
1131
+ event.preventDefault()
1132
+ toggleShowBranchChanges()
1133
+ }
1134
+ window.addEventListener('keydown', onKey)
1135
+ return () => window.removeEventListener('keydown', onKey)
1136
+ }, [canToggleBranchChanges, toggleShowBranchChanges])
1137
+
1138
+ useEffect(() => {
1139
+ let cancelled = false
1140
+ const load = async () => {
1141
+ const next = await fetchBranchChanges()
1142
+ if (!cancelled) setBranchChanges(next)
1143
+ }
1144
+ void load()
1145
+ if (!wantBranchChanges || llmBusy) {
1146
+ return () => {
1147
+ cancelled = true
1148
+ }
1149
+ }
1150
+ const timer = window.setInterval(() => {
1151
+ void load()
1152
+ }, 2000)
1153
+ return () => {
1154
+ cancelled = true
1155
+ window.clearInterval(timer)
1156
+ }
1157
+ }, [llmBusy, updatingModel, wantBranchChanges])
1158
+
1034
1159
  useEffect(() => {
1035
1160
  if (mode !== 'map' || !hasChangeSet) return
1036
1161
  const onKey = (event: KeyboardEvent) => {
@@ -1111,23 +1236,23 @@ function Explorer({
1111
1236
  void poll()
1112
1237
  const timer = window.setInterval(() => {
1113
1238
  void poll()
1114
- }, 700)
1239
+ }, 250)
1115
1240
  return () => {
1116
1241
  cancelled = true
1117
1242
  window.clearInterval(timer)
1118
1243
  }
1119
1244
  }, [])
1120
1245
 
1121
- const plannedIds = previewing ? [...intent.files, ...intent.creates] : []
1246
+ const plannedIds = previewing ? [...changeSet.files, ...changeSet.creates] : []
1122
1247
  const blueprintImportEdges = blueprintImports.flatMap((item) => {
1123
1248
  if (!displayGraph.files.some((file) => file.id === item.from)) return []
1124
1249
  return [{ from: item.file, to: item.from }]
1125
1250
  })
1126
1251
  const plannedImports = [
1127
- ...(previewing ? (intent.imports ?? []) : []),
1252
+ ...(previewing ? (changeSet.imports ?? []) : []),
1128
1253
  ...blueprintImportEdges,
1129
1254
  ]
1130
- const deletedIds = previewing ? intent.deletes : []
1255
+ const deletedIds = previewing ? changeSet.deletes : []
1131
1256
 
1132
1257
  return (
1133
1258
  <>
@@ -1162,7 +1287,7 @@ function Explorer({
1162
1287
  plannedImports={plannedImports}
1163
1288
  createdIds={plannedCreates}
1164
1289
  deletedIds={deletedIds}
1165
- createLines={intent.createLines ?? {}}
1290
+ createLines={changeSet.createLines ?? {}}
1166
1291
  flyTo={flyTo}
1167
1292
  aimedRelation={aimedRelation}
1168
1293
  onAimRelation={setAimedRelation}
@@ -1174,6 +1299,7 @@ function Explorer({
1174
1299
  namingIslandId={namingIslandId}
1175
1300
  onPlaceBlock={canPlace ? placeBlock : undefined}
1176
1301
  onPlaceIsland={canPlace ? placeIsland : undefined}
1302
+ onBlueprintMenu={canPlace ? setMapMenu : undefined}
1177
1303
  onCommitName={commitBlockName}
1178
1304
  onCancelName={cancelBlockName}
1179
1305
  userCreatedBlocks={userBlocks}
@@ -1203,12 +1329,18 @@ function Explorer({
1203
1329
  intents={intents}
1204
1330
  focusedSessionId={focusedSessionId}
1205
1331
  onFocusSession={focusSessionPanel}
1332
+ onSetupSession={setupLlmSession}
1206
1333
  onWorkflowAction={runWorkflowAction}
1207
1334
  onNavigateDiff={navigateDiff}
1208
1335
  onOpenMap={openMap}
1209
1336
  onWalk={openWalk}
1210
1337
  followLook={followLook}
1211
1338
  onToggleFollowLook={toggleFollowLook}
1339
+ showBranchChanges={showingBranchChanges}
1340
+ branchChanges={branchChanges}
1341
+ canShowBranchChanges={canToggleBranchChanges}
1342
+ llmMakingChanges={llmBusy}
1343
+ onToggleShowBranchChanges={toggleShowBranchChanges}
1212
1344
  onUpdateModel={onUpdateModel}
1213
1345
  updatingModel={updatingModel}
1214
1346
  importedBy={importedBy}
@@ -1241,6 +1373,12 @@ function Explorer({
1241
1373
  createdIds={plannedCreates}
1242
1374
  deletedIds={deletedIds}
1243
1375
  />
1376
+ <MapContextMenu
1377
+ menu={mapMenu}
1378
+ onAddFile={placeBlockOnFolder}
1379
+ onAddFolder={placeIslandOnFolder}
1380
+ onClose={() => setMapMenu(null)}
1381
+ />
1244
1382
  </>
1245
1383
  )
1246
1384
  }
@@ -45,10 +45,26 @@ function normalizeImportAdditions(value: unknown): PatchImportAddition[] {
45
45
  })
46
46
  }
47
47
 
48
+ function normalizeAck(
49
+ value: unknown,
50
+ ): AgentIntent['lastAck'] {
51
+ if (!value || typeof value !== 'object') return null
52
+ const kind = (value as { kind?: unknown }).kind
53
+ if (typeof kind !== 'string' || kind.trim() === '') return null
54
+ const detail = (value as { detail?: unknown }).detail
55
+ const at = (value as { at?: unknown }).at
56
+ return {
57
+ kind,
58
+ detail: typeof detail === 'string' ? detail : '',
59
+ at: typeof at === 'string' ? at : null,
60
+ }
61
+ }
62
+
48
63
  export const emptyIntent: AgentIntent = {
49
64
  updatedAt: null,
50
65
  showMap: false,
51
66
  status: 'idle',
67
+ name: null,
52
68
  feature: null,
53
69
  steps: [],
54
70
  step: null,
@@ -76,6 +92,10 @@ export const emptyIntent: AgentIntent = {
76
92
  working: false,
77
93
  stalledWait: false,
78
94
  llmIdle: false,
95
+ awaitingAttach: false,
96
+ listening: false,
97
+ lastAck: null,
98
+ initialInstruction: null,
79
99
  creationMode: false,
80
100
  canEnterBlueprint: false,
81
101
  blueprintSessionId: null,
@@ -91,6 +111,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
91
111
  updatedAt: data?.updatedAt ?? null,
92
112
  showMap: Boolean(data?.showMap),
93
113
  status: data?.status ?? 'idle',
114
+ name: data?.name ?? null,
94
115
  feature: data?.feature ?? null,
95
116
  steps: Array.isArray(data?.steps) ? data.steps : [],
96
117
  step: typeof data?.step === 'number' ? data.step : null,
@@ -121,6 +142,11 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
121
142
  working: Boolean(data?.working),
122
143
  stalledWait: Boolean(data?.stalledWait),
123
144
  llmIdle: Boolean(data?.llmIdle),
145
+ awaitingAttach: Boolean(data?.awaitingAttach),
146
+ listening: Boolean(data?.listening),
147
+ lastAck: normalizeAck(data?.lastAck),
148
+ initialInstruction:
149
+ typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
124
150
  creationMode: Boolean(data?.creationMode),
125
151
  canEnterBlueprint: Boolean(data?.canEnterBlueprint),
126
152
  blueprintSessionId:
@@ -224,6 +250,20 @@ export function persistSessionBlueprint(
224
250
  })
225
251
  }
226
252
 
253
+ export function persistInitialInstruction(sessionId: string, instruction: string) {
254
+ fetch('/api/agent-intent', {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({
258
+ action: 'set_initial_instruction',
259
+ sessionId,
260
+ instruction,
261
+ }),
262
+ }).catch(() => {
263
+ // Keep the local instruction if the session handshake is no longer open.
264
+ })
265
+ }
266
+
227
267
  export function persistSessionFocus(sessionId: string) {
228
268
  fetch('/api/agent-intent', {
229
269
  method: 'POST',
@@ -237,6 +277,29 @@ export function persistSessionFocus(sessionId: string) {
237
277
  })
238
278
  }
239
279
 
280
+ export async function setupVisualizerSession(name?: string) {
281
+ const response = await fetch('/api/agent-intent', {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({
285
+ action: 'setup_session',
286
+ name,
287
+ }),
288
+ })
289
+ if (!response.ok) {
290
+ const detail = await response.text()
291
+ let message = detail || 'Could not set up the LLM session'
292
+ try {
293
+ const parsed = JSON.parse(detail) as { error?: string }
294
+ if (parsed?.error) message = parsed.error
295
+ } catch {
296
+ // Use the raw body when it is not JSON.
297
+ }
298
+ throw new Error(message)
299
+ }
300
+ return normalize((await response.json()) as AgentIntent)
301
+ }
302
+
240
303
  export async function inspectTargetFile(payload: {
241
304
  sessionId?: string | null
242
305
  diffId?: string | null
@@ -0,0 +1,54 @@
1
+ import type { BranchChanges } from './types'
2
+
3
+ export const emptyBranchChanges: BranchChanges = {
4
+ available: false,
5
+ branch: null,
6
+ base: null,
7
+ files: [],
8
+ creates: [],
9
+ deletes: [],
10
+ createFolders: [],
11
+ createLines: {},
12
+ imports: [],
13
+ addedFunctions: [],
14
+ addedVariables: [],
15
+ addedImports: [],
16
+ changedFunctions: [],
17
+ changedVariables: [],
18
+ }
19
+
20
+ function normalize(data: Partial<BranchChanges> | null | undefined): BranchChanges {
21
+ return {
22
+ available: Boolean(data?.available),
23
+ branch: typeof data?.branch === 'string' ? data.branch : null,
24
+ base: typeof data?.base === 'string' ? data.base : null,
25
+ files: Array.isArray(data?.files) ? data.files : [],
26
+ creates: Array.isArray(data?.creates) ? data.creates : [],
27
+ deletes: Array.isArray(data?.deletes) ? data.deletes : [],
28
+ createFolders: Array.isArray(data?.createFolders) ? data.createFolders : [],
29
+ createLines:
30
+ data?.createLines && typeof data.createLines === 'object'
31
+ ? data.createLines
32
+ : {},
33
+ imports: Array.isArray(data?.imports) ? data.imports : [],
34
+ addedFunctions: Array.isArray(data?.addedFunctions) ? data.addedFunctions : [],
35
+ addedVariables: Array.isArray(data?.addedVariables) ? data.addedVariables : [],
36
+ addedImports: Array.isArray(data?.addedImports) ? data.addedImports : [],
37
+ changedFunctions: Array.isArray(data?.changedFunctions)
38
+ ? data.changedFunctions
39
+ : [],
40
+ changedVariables: Array.isArray(data?.changedVariables)
41
+ ? data.changedVariables
42
+ : [],
43
+ }
44
+ }
45
+
46
+ export async function fetchBranchChanges(): Promise<BranchChanges> {
47
+ try {
48
+ const response = await fetch(`/api/branch-changes?t=${Date.now()}`)
49
+ if (!response.ok) return emptyBranchChanges
50
+ return normalize((await response.json()) as BranchChanges)
51
+ } catch {
52
+ return emptyBranchChanges
53
+ }
54
+ }