@jkwd/inbase 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -53,7 +53,7 @@ Sessions start in the map: click **Setup LLM session**, type an **initial instru
53
53
 
54
54
  A normal chat request does not open a session. Use `/inbase` after Setup LLM session, or `/skipinbase [request]` to work outside the map. `/inbase` starts the session immediately; `wait-for-blueprint` only reads the optional blueprint.
55
55
 
56
- If several sessions are open, only one session window is shown at a time. Switch tabs so the focused session is the one `/inbase` connects to.
56
+ If several sessions are open, `/inbase` attaches the newest session that is still waiting. Sessions that already have an LLM are skipped, and the map window does not need to be focused.
57
57
 
58
58
  Turn on **Make LLM look where I look** if the agent should prefer the island you are standing on and the blocks you are facing. With **Step by step** on, click **Create proposal** to start a step, then **Accept proposal** when the patch is ready. With it off, the LLM implements the full plan; you can still walk Previous/Next over the diffs, then **Accept proposal**. Send an alternative instruction from the HUD to revise the remaining plan, or **Stop** to end the session.
59
59
 
@@ -130,6 +130,8 @@ export function setInitialInstruction(
130
130
  instruction: string | null | undefined,
131
131
  ): DiffManifest
132
132
  export function readAttachedSession(dataDir: string): string | null
133
+ export function listAttachQueue(dataDir: string): string[]
134
+ export function nextAttachSessionId(dataDir: string): string | null
133
135
  export type SessionBlueprint = {
134
136
  enabled: boolean
135
137
  sent: boolean
@@ -341,6 +341,74 @@ export function focusSession(dataDir, sessionId) {
341
341
  return safeId
342
342
  }
343
343
 
344
+ function attachQueueFile(dataDir) {
345
+ return path.join(dataDir, 'attach-queue.json')
346
+ }
347
+
348
+ function readStoredAttachQueue(dataDir) {
349
+ const value = readJson(attachQueueFile(dataDir), null)
350
+ const ids = Array.isArray(value?.sessionIds) ? value.sessionIds : []
351
+ const result = []
352
+ const seen = new Set()
353
+ for (const id of ids) {
354
+ try {
355
+ const safeId = assertSessionId(id)
356
+ if (seen.has(safeId)) continue
357
+ seen.add(safeId)
358
+ result.push(safeId)
359
+ } catch {
360
+ // Skip invalid ids.
361
+ }
362
+ }
363
+ return result
364
+ }
365
+
366
+ function writeAttachQueue(dataDir, sessionIds) {
367
+ atomicWrite(
368
+ attachQueueFile(dataDir),
369
+ `${JSON.stringify({ sessionIds }, null, 2)}\n`,
370
+ )
371
+ }
372
+
373
+ function sessionIsWaitingToAttach(manifest) {
374
+ return Boolean(manifest?.awaitingAttach)
375
+ }
376
+
377
+ export function listAttachQueue(dataDir) {
378
+ const recorded = readStoredAttachQueue(dataDir)
379
+ const waiting = new Set()
380
+ for (const sessionId of listOpenSessionIds(dataDir)) {
381
+ if (sessionIsWaitingToAttach(readManifest(dataDir, sessionId))) {
382
+ waiting.add(sessionId)
383
+ }
384
+ }
385
+ const queued = recorded.filter((sessionId) => waiting.has(sessionId))
386
+ const queuedSet = new Set(queued)
387
+ const missing = []
388
+ for (const sessionId of listOpenSessionIds(dataDir)) {
389
+ if (waiting.has(sessionId) && !queuedSet.has(sessionId)) {
390
+ missing.push(sessionId)
391
+ }
392
+ }
393
+ missing.reverse()
394
+ const next = [...queued, ...missing]
395
+ const unchanged =
396
+ next.length === recorded.length &&
397
+ next.every((sessionId, index) => sessionId === recorded[index])
398
+ if (!unchanged) writeAttachQueue(dataDir, next)
399
+ return next
400
+ }
401
+
402
+ export function nextAttachSessionId(dataDir) {
403
+ return listAttachQueue(dataDir)[0] ?? null
404
+ }
405
+
406
+ function enqueueAttachSession(dataDir, sessionId) {
407
+ const safeId = assertSessionId(sessionId)
408
+ const rest = listAttachQueue(dataDir).filter((id) => id !== safeId)
409
+ writeAttachQueue(dataDir, [safeId, ...rest])
410
+ }
411
+
344
412
  function sessionAllowsPlacement(manifest) {
345
413
  return (
346
414
  manifest.phase !== 'blueprint_ask' &&
@@ -912,6 +980,7 @@ export function setupSession(dataDir, input = {}) {
912
980
  sent: false,
913
981
  })
914
982
  focusSession(dataDir, sessionId)
983
+ enqueueAttachSession(dataDir, sessionId)
915
984
  return manifest
916
985
  }
917
986
 
@@ -952,10 +1021,10 @@ export function readAttachedSession(dataDir) {
952
1021
  export function attachSession(dataDir, sessionId) {
953
1022
  const safeId = sessionId
954
1023
  ? assertSessionId(sessionId)
955
- : readActiveSession(dataDir)
1024
+ : nextAttachSessionId(dataDir)
956
1025
  if (!safeId) {
957
1026
  throw new Error(
958
- 'No visualizer session is focused. Click Setup LLM session in the map, then /inbase.',
1027
+ 'No visualizer session is waiting to attach. Click Setup LLM session in the map, then /inbase.',
959
1028
  )
960
1029
  }
961
1030
  const manifest = requireManifest(
@@ -966,14 +1035,6 @@ export function attachSession(dataDir, sessionId) {
966
1035
  if (isTerminalSession(manifest)) {
967
1036
  throw sessionStoppedError(safeId)
968
1037
  }
969
- const attached = readAttachedSession(dataDir)
970
- if (attached && attached !== safeId) {
971
- const other = readManifest(dataDir, attached)
972
- const label = resolvedSessionName(other) || attached
973
- throw new Error(
974
- `An LLM is already attached to ${label}. Stop that session before attaching another.`,
975
- )
976
- }
977
1038
  focusSession(dataDir, safeId)
978
1039
  touchSessionConnection(dataDir, safeId)
979
1040
  recordSessionAck(dataDir, safeId, 'attached', resolvedSessionName(manifest) || safeId)
@@ -1435,6 +1496,7 @@ export function clearDiffSessions(dataDir, targetRoot = null) {
1435
1496
  }
1436
1497
  writeActiveSession(dataDir, null)
1437
1498
  writeBlueprintSession(dataDir, null)
1499
+ writeAttachQueue(dataDir, [])
1438
1500
 
1439
1501
  const root = diffSessionsRoot(dataDir)
1440
1502
  fs.mkdirSync(root, { recursive: true })
@@ -14,6 +14,10 @@ import {
14
14
  } from './layout'
15
15
  import { World } from './scene/World'
16
16
  import { HUD } from './ui/HUD'
17
+ import {
18
+ MapContextMenu,
19
+ type MapContextMenuState,
20
+ } from './ui/MapContextMenu'
17
21
  import {
18
22
  fetchUserContext,
19
23
  persistFollowLook,
@@ -152,6 +156,9 @@ function Explorer({
152
156
  }) {
153
157
  const [intents, setIntents] = useState<AgentIntent[]>([])
154
158
  const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null)
159
+ const [nextAttachSessionId, setNextAttachSessionId] = useState<string | null>(
160
+ null,
161
+ )
155
162
  const [wantBranchChanges, setWantBranchChanges] = useState(false)
156
163
  const [branchChanges, setBranchChanges] = useState(emptyBranchChanges)
157
164
  const intent =
@@ -222,7 +229,9 @@ function Explorer({
222
229
  ],
223
230
  )
224
231
  const layout = useMemo(() => {
225
- const world = layoutWorld(previewGraph)
232
+ const world = layoutWorld(
233
+ withUserCreatedGraph(previewGraph, [], userIslands),
234
+ )
226
235
  if (previewing) markCreatedFolders(world, changeSet.createFolders ?? [])
227
236
  return withUserCreatedLayout(world, userBlocks, userIslands)
228
237
  }, [
@@ -294,6 +303,7 @@ function Explorer({
294
303
  const [selectedId, setSelectedId] = useState<string | null>(null)
295
304
  const [selectedTick, setSelectedTick] = useState(0)
296
305
  const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
306
+ const [mapMenu, setMapMenu] = useState<MapContextMenuState | null>(null)
297
307
  const [aimedRelation, setAimedRelation] = useState<AimedRelation | null>(null)
298
308
  const [aimedFileId, setAimedFileId] = useState<string | null>(null)
299
309
  const [inspectTick, setInspectTick] = useState(0)
@@ -725,12 +735,14 @@ function Explorer({
725
735
  const placeIsland = useCallback(
726
736
  (parent: string) => {
727
737
  if (!canPlace) return
738
+ let placedId: string | null = null
728
739
  setUserIslands((current) => {
729
740
  if (current.some((island) => island.naming)) return current
741
+ placedId = `draft:${Date.now()}`
730
742
  return [
731
743
  ...current,
732
744
  {
733
- id: `draft:${Date.now()}`,
745
+ id: placedId,
734
746
  name: '',
735
747
  path: '',
736
748
  parent,
@@ -738,6 +750,10 @@ function Explorer({
738
750
  },
739
751
  ]
740
752
  })
753
+ if (placedId) {
754
+ setSelectedFolder(placedId)
755
+ setSelectedId(null)
756
+ }
741
757
  document.exitPointerLock()
742
758
  },
743
759
  [canPlace],
@@ -772,14 +788,23 @@ function Explorer({
772
788
  )
773
789
  setUserIslands(next)
774
790
  persistBlueprint(userBlocks, next)
791
+ setSelectedFolder(resolved.path)
792
+ setSelectedId(null)
775
793
  return true
776
794
  },
777
795
  [graph.folders, persistBlueprint, userBlocks, userIslands],
778
796
  )
779
797
 
780
- const cancelIslandName = useCallback((id: string) => {
781
- setUserIslands((current) => current.filter((island) => island.id !== id))
782
- }, [])
798
+ const cancelIslandName = useCallback(
799
+ (id: string) => {
800
+ const draft = userIslands.find((island) => island.id === id)
801
+ setUserIslands((current) => current.filter((island) => island.id !== id))
802
+ setSelectedFolder((selected) =>
803
+ selected === id ? draft?.parent ?? null : selected,
804
+ )
805
+ },
806
+ [userIslands],
807
+ )
783
808
 
784
809
  const deleteSelectedCreatedBlock = useCallback(() => {
785
810
  if (!canPlace || !selectedId) return false
@@ -1019,6 +1044,10 @@ function Explorer({
1019
1044
  return () => window.removeEventListener('keydown', onKey)
1020
1045
  }, [cancelBlockName, cancelIslandName, namingId, namingIslandId])
1021
1046
 
1047
+ useEffect(() => {
1048
+ if (mode !== 'map' || naming) setMapMenu(null)
1049
+ }, [mode, naming])
1050
+
1022
1051
  useEffect(() => {
1023
1052
  const onKey = (event: KeyboardEvent) => {
1024
1053
  if (event.repeat || event.code !== 'Backspace') return
@@ -1156,6 +1185,8 @@ function Explorer({
1156
1185
  const poll = async () => {
1157
1186
  try {
1158
1187
  const bundle = await fetchAgentIntents()
1188
+ if (cancelled) return
1189
+ setNextAttachSessionId(bundle.nextAttachSessionId)
1159
1190
  const merged: AgentIntent[] = []
1160
1191
  for (const next of bundle.intents) {
1161
1192
  const sessionId = next.sessionId
@@ -1273,6 +1304,7 @@ function Explorer({
1273
1304
  namingIslandId={namingIslandId}
1274
1305
  onPlaceBlock={canPlace ? placeBlock : undefined}
1275
1306
  onPlaceIsland={canPlace ? placeIsland : undefined}
1307
+ onBlueprintMenu={canPlace ? setMapMenu : undefined}
1276
1308
  onCommitName={commitBlockName}
1277
1309
  onCancelName={cancelBlockName}
1278
1310
  userCreatedBlocks={userBlocks}
@@ -1301,6 +1333,7 @@ function Explorer({
1301
1333
  intent={intent}
1302
1334
  intents={intents}
1303
1335
  focusedSessionId={focusedSessionId}
1336
+ nextAttachSessionId={nextAttachSessionId}
1304
1337
  onFocusSession={focusSessionPanel}
1305
1338
  onSetupSession={setupLlmSession}
1306
1339
  onWorkflowAction={runWorkflowAction}
@@ -1346,6 +1379,12 @@ function Explorer({
1346
1379
  createdIds={plannedCreates}
1347
1380
  deletedIds={deletedIds}
1348
1381
  />
1382
+ <MapContextMenu
1383
+ menu={mapMenu}
1384
+ onAddFile={placeBlockOnFolder}
1385
+ onAddFolder={placeIslandOnFolder}
1386
+ onClose={() => setMapMenu(null)}
1387
+ />
1349
1388
  </>
1350
1389
  )
1351
1390
  }
@@ -164,9 +164,10 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
164
164
  export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
165
165
  const query = new URLSearchParams({ t: String(Date.now()) })
166
166
  const response = await fetch(`/api/agent-intent?${query}`)
167
- if (!response.ok) return { focusedSessionId: null, intents: [] }
167
+ if (!response.ok) return { focusedSessionId: null, nextAttachSessionId: null, intents: [] }
168
168
  const data = (await response.json()) as {
169
169
  focusedSessionId?: string | null
170
+ nextAttachSessionId?: string | null
170
171
  intents?: unknown
171
172
  sessionId?: string | null
172
173
  } & Partial<AgentIntent>
@@ -174,6 +175,10 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
174
175
  return {
175
176
  focusedSessionId:
176
177
  typeof data.focusedSessionId === 'string' ? data.focusedSessionId : null,
178
+ nextAttachSessionId:
179
+ typeof data.nextAttachSessionId === 'string'
180
+ ? data.nextAttachSessionId
181
+ : null,
177
182
  intents: data.intents
178
183
  .map((intent) => normalize(intent as Partial<AgentIntent>))
179
184
  .filter((intent) => Boolean(intent.sessionId)),
@@ -182,6 +187,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
182
187
  const intent = normalize(data)
183
188
  return {
184
189
  focusedSessionId: intent.sessionId,
190
+ nextAttachSessionId: intent.awaitingAttach ? intent.sessionId : null,
185
191
  intents: intent.sessionId ? [intent] : [],
186
192
  }
187
193
  }
@@ -202,7 +202,18 @@ button {
202
202
  letter-spacing: 0.02em;
203
203
  }
204
204
 
205
+ .map-folder-label-layer {
206
+ position: absolute;
207
+ inset: 0;
208
+ overflow: hidden;
209
+ pointer-events: none;
210
+ z-index: 80;
211
+ }
212
+
205
213
  .map-folder-label {
214
+ position: absolute;
215
+ top: 0;
216
+ left: 0;
206
217
  display: grid;
207
218
  min-width: max-content;
208
219
  padding: 4px 7px;
@@ -213,6 +224,8 @@ button {
213
224
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
214
225
  text-align: center;
215
226
  white-space: nowrap;
227
+ visibility: hidden;
228
+ will-change: transform;
216
229
  }
217
230
 
218
231
  .map-folder-name {
@@ -561,14 +574,38 @@ button {
561
574
  }
562
575
 
563
576
  .hud-session-tab {
577
+ display: inline-flex;
578
+ align-items: center;
579
+ justify-content: center;
580
+ gap: 7px;
564
581
  flex: 1 1 0;
565
582
  min-width: 0;
566
583
  overflow: hidden;
567
- text-overflow: ellipsis;
568
584
  white-space: nowrap;
569
585
  font-size: 13px;
570
586
  }
571
587
 
588
+ .hud-session-tab-label {
589
+ min-width: 0;
590
+ overflow: hidden;
591
+ text-overflow: ellipsis;
592
+ }
593
+
594
+ .hud-session-tab[data-attached='true'] {
595
+ border-color: #6ee7b7;
596
+ color: #b6f3d6;
597
+ }
598
+
599
+ .hud-session-tab[data-attached='true'] .hud-attach-dot {
600
+ background: #6ee7b7;
601
+ box-shadow: 0 0 8px #6ee7b7;
602
+ }
603
+
604
+ .hud-session-tab[data-attached='false'] .hud-attach-dot {
605
+ background: #6b7280;
606
+ box-shadow: none;
607
+ }
608
+
572
609
  .hud-left-stack .hud-panel-planned {
573
610
  position: relative;
574
611
  top: auto;
@@ -593,6 +630,14 @@ button {
593
630
  box-shadow: 0 0 0 1px #9ad8ff;
594
631
  }
595
632
 
633
+ .hud-left-stack .hud-panel-planned[data-attached='true'] {
634
+ box-shadow: 0 0 0 1px #6ee7b7;
635
+ }
636
+
637
+ .hud-left-stack .hud-panel-planned[data-attached='true'][data-focused='true'] {
638
+ box-shadow: 0 0 0 1px #6ee7b7, 0 0 0 3px rgba(110, 231, 183, 0.22);
639
+ }
640
+
596
641
  .hud-panel-body {
597
642
  min-height: 0;
598
643
  flex: 1 1 auto;
@@ -665,6 +710,65 @@ button {
665
710
  flex: 1 1 auto;
666
711
  }
667
712
 
713
+ .hud-panel-chrome-title-row {
714
+ display: flex;
715
+ align-items: center;
716
+ gap: 8px;
717
+ min-width: 0;
718
+ }
719
+
720
+ .hud-attach-badge {
721
+ display: inline-flex;
722
+ align-items: center;
723
+ gap: 6px;
724
+ flex: none;
725
+ padding: 2px 7px 2px 6px;
726
+ border: 1px solid rgba(110, 231, 183, 0.45);
727
+ border-radius: 999px;
728
+ background: rgba(110, 231, 183, 0.12);
729
+ color: #b6f3d6;
730
+ font-size: 10px;
731
+ font-weight: 700;
732
+ letter-spacing: 0.06em;
733
+ text-transform: uppercase;
734
+ }
735
+
736
+ .hud-attach-badge[data-attached='false'] {
737
+ border-color: rgba(255, 255, 255, 0.16);
738
+ background: rgba(255, 255, 255, 0.04);
739
+ color: #8b95a5;
740
+ }
741
+
742
+ .hud-attach-dot {
743
+ width: 7px;
744
+ height: 7px;
745
+ flex: none;
746
+ border-radius: 50%;
747
+ background: #6ee7b7;
748
+ box-shadow: 0 0 8px #6ee7b7;
749
+ }
750
+
751
+ .hud-attach-badge[data-attached='true'] .hud-attach-dot {
752
+ animation: hud-attach-pulse 1.6s ease infinite;
753
+ }
754
+
755
+ .hud-attach-badge[data-attached='false'] .hud-attach-dot {
756
+ background: #6b7280;
757
+ box-shadow: none;
758
+ }
759
+
760
+ @keyframes hud-attach-pulse {
761
+ 0%,
762
+ 100% {
763
+ opacity: 1;
764
+ box-shadow: 0 0 8px #6ee7b7;
765
+ }
766
+ 50% {
767
+ opacity: 0.55;
768
+ box-shadow: 0 0 2px #6ee7b7;
769
+ }
770
+ }
771
+
668
772
  .hud-panel[data-minimized='true'] {
669
773
  overflow: hidden;
670
774
  padding-bottom: 12px;
@@ -676,6 +780,7 @@ button {
676
780
 
677
781
  .hud-panel-chrome-title {
678
782
  min-width: 0;
783
+ flex: 1 1 auto;
679
784
  overflow: hidden;
680
785
  color: #b7c0ce;
681
786
  font-size: 11px;
@@ -1112,6 +1217,35 @@ button {
1112
1217
  margin-top: 14px;
1113
1218
  }
1114
1219
 
1220
+ .map-context-menu {
1221
+ position: fixed;
1222
+ z-index: 40;
1223
+ display: flex;
1224
+ min-width: 168px;
1225
+ flex-direction: column;
1226
+ gap: 2px;
1227
+ padding: 4px;
1228
+ color: #e7ebf2;
1229
+ background: rgba(25, 28, 34, 0.96);
1230
+ border: 1px solid var(--vc-border);
1231
+ pointer-events: auto;
1232
+ }
1233
+
1234
+ .map-context-menu button {
1235
+ cursor: pointer;
1236
+ padding: 8px 10px;
1237
+ color: inherit;
1238
+ text-align: left;
1239
+ background: transparent;
1240
+ border: 0;
1241
+ }
1242
+
1243
+ .map-context-menu button:hover,
1244
+ .map-context-menu button:focus-visible {
1245
+ background: var(--vc-surface);
1246
+ outline: none;
1247
+ }
1248
+
1115
1249
  .hud-decide {
1116
1250
  display: flex;
1117
1251
  flex-wrap: wrap;
@@ -0,0 +1,44 @@
1
+ import { useRef } from 'react'
2
+ import { Instances, Instance } from '@react-three/drei'
3
+ import { dimColor, fileColor } from '../theme'
4
+ import type { FileNode, PlacedFile } from '../types'
5
+
6
+ type DistantFile = {
7
+ file: FileNode
8
+ placed: PlacedFile
9
+ dimmed: boolean
10
+ }
11
+
12
+ export function DistantFileBlocks({
13
+ items,
14
+ }: {
15
+ items: DistantFile[]
16
+ }) {
17
+ const cap = useRef(2048)
18
+ if (items.length > cap.current) cap.current = items.length
19
+ if (items.length === 0) return null
20
+
21
+ return (
22
+ <Instances
23
+ limit={cap.current}
24
+ range={items.length}
25
+ frustumCulled
26
+ raycast={() => {}}
27
+ >
28
+ <boxGeometry args={[1, 1, 1]} />
29
+ <meshLambertMaterial />
30
+ {items.map(({ file, placed, dimmed }) => {
31
+ const color = file.userCreated ? '#7ec8e8' : fileColor(file.language)
32
+ return (
33
+ <Instance
34
+ key={file.id}
35
+ position={placed.position}
36
+ scale={placed.size}
37
+ color={dimmed ? dimColor(color, 0.32) : color}
38
+ userData={{ fileId: file.id }}
39
+ />
40
+ )
41
+ })}
42
+ </Instances>
43
+ )
44
+ }
@@ -1,6 +1,5 @@
1
- import { Suspense, useRef } from 'react'
1
+ import { memo, Suspense } from 'react'
2
2
  import { Billboard, Edges, Html, Text } from '@react-three/drei'
3
- import { useFrame, useThree } from '@react-three/fiber'
4
3
  import { CHANGE_HIGHLIGHT, CONFIG, dimColor, fileColor, FILE_SELECTION, MAP_SELECTION, type ChangeKind } from '../theme'
5
4
  import { NameInput } from '../ui/NameInput'
6
5
  import { MapSelectBorder } from './MapSelectBorder'
@@ -59,33 +58,27 @@ function MapChangeMark({
59
58
  depth: number
60
59
  height: number
61
60
  }) {
62
- const camera = useThree((state) => state.camera)
63
- const markRef = useRef<HTMLDivElement>(null)
64
-
65
- useFrame(() => {
66
- const el = markRef.current
67
- if (!el) return
68
- const zoom = 'zoom' in camera ? Number(camera.zoom) : 1
69
- const px = Math.min(width, depth) * 0.78 * zoom
70
- el.style.fontSize = `${Math.max(px, 1)}px`
71
- })
72
-
61
+ const size = Math.min(width, depth) * 0.78
73
62
  return (
74
- <Html
75
- position={[0, height / 2 + 0.12, 0]}
76
- center
77
- occlude={false}
78
- zIndexRange={[120, 100]}
79
- style={{ pointerEvents: 'none' }}
80
- >
81
- <div ref={markRef} className="map-change-mark">
63
+ <Suspense fallback={null}>
64
+ <Text
65
+ position={[0, height / 2 + 0.12, 0]}
66
+ rotation={[-Math.PI / 2, 0, 0]}
67
+ fontSize={size}
68
+ color="#f4f7fb"
69
+ anchorX="center"
70
+ anchorY="middle"
71
+ outlineWidth={size * 0.09}
72
+ outlineColor="#11151c"
73
+ renderOrder={10}
74
+ >
82
75
  {mark}
83
- </div>
84
- </Html>
76
+ </Text>
77
+ </Suspense>
85
78
  )
86
79
  }
87
80
 
88
- export function FileBlock({
81
+ export const FileBlock = memo(function FileBlock({
89
82
  file,
90
83
  placed,
91
84
  selected,
@@ -132,7 +125,7 @@ export function FileBlock({
132
125
  ? muted
133
126
  : color
134
127
 
135
- const showLabels = !naming && !mapMode
128
+ const showLabels = !naming && !mapMode && labelVisible
136
129
 
137
130
  return (
138
131
  <group position={placed.position}>
@@ -216,17 +209,15 @@ export function FileBlock({
216
209
  {showLabels && (
217
210
  <Suspense fallback={null}>
218
211
  {previewLabels ? (
219
- labelVisible && (
220
- <Html
221
- position={[0, height / 2 + 0.4, 0]}
222
- center
223
- occlude={false}
224
- style={{ pointerEvents: 'none' }}
225
- zIndexRange={[20, 0]}
226
- >
227
- <div className="thumbnail-block-label">{label}</div>
228
- </Html>
229
- )
212
+ <Html
213
+ position={[0, height / 2 + 0.4, 0]}
214
+ center
215
+ occlude={false}
216
+ style={{ pointerEvents: 'none' }}
217
+ zIndexRange={[20, 0]}
218
+ >
219
+ <div className="thumbnail-block-label">{label}</div>
220
+ </Html>
230
221
  ) : (
231
222
  <>
232
223
  <Billboard position={[0, height / 2 + (planned || highlight ? 0.55 : 0.38), 0]}>
@@ -272,4 +263,4 @@ export function FileBlock({
272
263
  )}
273
264
  </group>
274
265
  )
275
- }
266
+ })
@@ -18,6 +18,7 @@ type FolderAreaProps = {
18
18
  mapMode?: boolean
19
19
  highlightKind?: ChangeKind | null
20
20
  previewLabels?: boolean
21
+ labelVisible?: boolean
21
22
  }
22
23
 
23
24
  export function FolderArea({
@@ -27,6 +28,7 @@ export function FolderArea({
27
28
  mapMode = false,
28
29
  highlightKind = null,
29
30
  previewLabels = false,
31
+ labelVisible = true,
30
32
  }: FolderAreaProps) {
31
33
  const added = Boolean(folder.added)
32
34
  const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
@@ -87,7 +89,7 @@ export function FolderArea({
87
89
  </mesh>
88
90
  )}
89
91
  <Suspense fallback={null}>
90
- {!naming && previewLabels && (
92
+ {!naming && previewLabels && labelVisible && (
91
93
  <Html
92
94
  position={[0, 1.35, -folder.depth / 2 + 1.6]}
93
95
  center
@@ -98,7 +100,7 @@ export function FolderArea({
98
100
  <div className="thumbnail-folder-label">{label}</div>
99
101
  </Html>
100
102
  )}
101
- {!naming && !previewLabels && !mapMode && (
103
+ {!naming && !previewLabels && !mapMode && labelVisible && (
102
104
  <Text
103
105
  position={[0, 0.05, -folder.depth / 2 + 1.6]}
104
106
  rotation={[-Math.PI / 2, 0, 0]}