@jkwd/inbase 0.1.12 → 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.
@@ -0,0 +1,201 @@
1
+ import { CONFIG } from '../theme'
2
+ import type { PlacedBridge, PlacedFile, PlacedFolder } from '../types'
3
+
4
+ export const WALK_MESH_RADIUS = 120
5
+ export const WALK_MESH_KEEP = 148
6
+ export const WALK_LABEL_RADIUS = 28
7
+ export const WALK_LABEL_KEEP = 36
8
+ export const WALK_FOLDER_PAD = 160
9
+ export const WALK_FOLDER_LABEL = 18
10
+ export const WALK_AROUND = 14
11
+ export const WALK_BEHIND_DOT = -0.18
12
+
13
+ export type WalkLod = {
14
+ files: Set<string>
15
+ labels: Set<string>
16
+ folders: Set<string>
17
+ folderLabels: Set<string>
18
+ bridges: Set<string>
19
+ }
20
+
21
+ export function folderDistance(
22
+ px: number,
23
+ pz: number,
24
+ folder: PlacedFolder,
25
+ ): number {
26
+ const halfW = folder.width / 2
27
+ const minX = folder.x - halfW
28
+ const maxX = folder.x + halfW
29
+ const minZ = folder.z
30
+ const maxZ = folder.z + folder.depth
31
+ const cx = Math.min(maxX, Math.max(minX, px))
32
+ const cz = Math.min(maxZ, Math.max(minZ, pz))
33
+ return Math.hypot(px - cx, pz - cz)
34
+ }
35
+
36
+ function distanceToSegment(
37
+ px: number,
38
+ pz: number,
39
+ ax: number,
40
+ az: number,
41
+ bx: number,
42
+ bz: number,
43
+ ) {
44
+ const dx = bx - ax
45
+ const dz = bz - az
46
+ const lengthSq = dx * dx + dz * dz
47
+ if (lengthSq === 0) return Math.hypot(px - ax, pz - az)
48
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (pz - az) * dz) / lengthSq))
49
+ return Math.hypot(px - (ax + dx * t), pz - (az + dz * t))
50
+ }
51
+
52
+ function bridgeDistance(
53
+ px: number,
54
+ pz: number,
55
+ bridge: PlacedBridge,
56
+ ): number {
57
+ let nearest = Infinity
58
+ for (let i = 1; i < bridge.points.length; i += 1) {
59
+ const from = bridge.points[i - 1]
60
+ const to = bridge.points[i]
61
+ nearest = Math.min(
62
+ nearest,
63
+ distanceToSegment(px, pz, from[0], from[1], to[0], to[1]),
64
+ )
65
+ }
66
+ if (!Number.isFinite(nearest) && bridge.points[0]) {
67
+ return Math.hypot(px - bridge.points[0][0], pz - bridge.points[0][1])
68
+ }
69
+ return nearest
70
+ }
71
+
72
+ function inFrontOrNearby(
73
+ dx: number,
74
+ dz: number,
75
+ lookX: number,
76
+ lookZ: number,
77
+ ): boolean {
78
+ const distSq = dx * dx + dz * dz
79
+ if (distSq <= WALK_AROUND * WALK_AROUND) return true
80
+ const dist = Math.sqrt(distSq)
81
+ return (dx / dist) * lookX + (dz / dist) * lookZ >= WALK_BEHIND_DOT
82
+ }
83
+
84
+ export function walkLodCell(
85
+ x: number,
86
+ z: number,
87
+ y: number,
88
+ lookX: number,
89
+ lookZ: number,
90
+ ) {
91
+ const angle = Math.atan2(lookX, lookZ)
92
+ const bucket = Math.round(angle / (Math.PI / 10))
93
+ const heightBand = Math.round(Math.max(0, y - CONFIG.eyeHeight) / 6)
94
+ return `${Math.round(x / 4)}:${Math.round(z / 4)}:${heightBand}:${bucket}`
95
+ }
96
+
97
+ function sameSet(left: Set<string>, right: Set<string>) {
98
+ if (left.size !== right.size) return false
99
+ for (const id of left) {
100
+ if (!right.has(id)) return false
101
+ }
102
+ return true
103
+ }
104
+
105
+ export function sameWalkLod(left: WalkLod | null, right: WalkLod) {
106
+ if (!left) return false
107
+ return (
108
+ sameSet(left.files, right.files) &&
109
+ sameSet(left.labels, right.labels) &&
110
+ sameSet(left.folders, right.folders) &&
111
+ sameSet(left.folderLabels, right.folderLabels) &&
112
+ sameSet(left.bridges, right.bridges)
113
+ )
114
+ }
115
+
116
+ export function heightBoost(y: number) {
117
+ return Math.max(0, y - CONFIG.eyeHeight)
118
+ }
119
+
120
+ export function computeWalkLod({
121
+ x,
122
+ z,
123
+ y = CONFIG.eyeHeight,
124
+ lookX,
125
+ lookZ,
126
+ files,
127
+ folders,
128
+ bridges,
129
+ keepFileIds,
130
+ keepFolderPaths,
131
+ prev,
132
+ }: {
133
+ x: number
134
+ z: number
135
+ y?: number
136
+ lookX: number
137
+ lookZ: number
138
+ files: Record<string, PlacedFile>
139
+ folders: Record<string, PlacedFolder>
140
+ bridges: PlacedBridge[]
141
+ keepFileIds: Iterable<string>
142
+ keepFolderPaths: Iterable<string>
143
+ prev: WalkLod | null
144
+ }): WalkLod {
145
+ const boost = heightBoost(y)
146
+ const meshRadius = WALK_MESH_RADIUS + boost * 2.4
147
+ const meshKeep = WALK_MESH_KEEP + boost * 2.4
148
+ const labelRadius = WALK_LABEL_RADIUS + boost * 0.6
149
+ const labelKeep = WALK_LABEL_KEEP + boost * 0.6
150
+ const folderPad = WALK_FOLDER_PAD + boost * 3
151
+ const lookLen = Math.hypot(lookX, lookZ) || 1
152
+ const nx = lookX / lookLen
153
+ const nz = lookZ / lookLen
154
+
155
+ const nextFiles = new Set<string>()
156
+ const nextLabels = new Set<string>()
157
+ for (const id of keepFileIds) nextFiles.add(id)
158
+
159
+ for (const [id, placed] of Object.entries(files)) {
160
+ const dx = placed.position[0] - x
161
+ const dz = placed.position[2] - z
162
+ const dist = Math.hypot(dx, dz)
163
+ const kept = Boolean(prev?.files.has(id))
164
+ const labeled = Boolean(prev?.labels.has(id))
165
+ const meshLimit = kept ? meshKeep : meshRadius
166
+ const labelLimit = labeled ? labelKeep : labelRadius
167
+ if (dist > meshLimit) continue
168
+ if (!inFrontOrNearby(dx, dz, nx, nz) && dist > WALK_AROUND) continue
169
+ nextFiles.add(id)
170
+ if (dist <= labelLimit) nextLabels.add(id)
171
+ }
172
+
173
+ const nextFolders = new Set<string>()
174
+ const nextFolderLabels = new Set<string>()
175
+ for (const path of keepFolderPaths) nextFolders.add(path)
176
+
177
+ for (const folder of Object.values(folders)) {
178
+ const dist = folderDistance(x, z, folder)
179
+ const kept = Boolean(prev?.folders.has(folder.path))
180
+ const limit = kept ? folderPad + 18 : folderPad
181
+ if (dist > limit && !nextFolders.has(folder.path)) continue
182
+ nextFolders.add(folder.path)
183
+ if (dist <= WALK_FOLDER_LABEL) nextFolderLabels.add(folder.path)
184
+ }
185
+
186
+ const nextBridges = new Set<string>()
187
+ for (const bridge of bridges) {
188
+ const dist = bridgeDistance(x, z, bridge)
189
+ const kept = Boolean(prev?.bridges.has(bridge.id))
190
+ const limit = kept ? folderPad + 18 : folderPad
191
+ if (dist <= limit) nextBridges.add(bridge.id)
192
+ }
193
+
194
+ return {
195
+ files: nextFiles,
196
+ labels: nextLabels,
197
+ folders: nextFolders,
198
+ folderLabels: nextFolderLabels,
199
+ bridges: nextBridges,
200
+ }
201
+ }
@@ -255,6 +255,7 @@ export type WorkflowAction =
255
255
 
256
256
  export type AgentIntentBundle = {
257
257
  focusedSessionId: string | null
258
+ nextAttachSessionId: string | null
258
259
  intents: AgentIntent[]
259
260
  }
260
261
 
@@ -214,9 +214,23 @@ function AddIntentRow({
214
214
  )
215
215
  }
216
216
 
217
+ function AttachStateBadge({ attached }: { attached: boolean }) {
218
+ return (
219
+ <span
220
+ className="hud-attach-badge"
221
+ data-attached={attached}
222
+ aria-label={attached ? 'LLM attached' : 'Waiting for LLM'}
223
+ >
224
+ <span className="hud-attach-dot" aria-hidden="true" />
225
+ {attached ? 'Attached' : 'Waiting'}
226
+ </span>
227
+ )
228
+ }
229
+
217
230
  function PanelChrome({
218
231
  title,
219
232
  subtitle,
233
+ badge,
220
234
  minimized = false,
221
235
  onMinimize,
222
236
  onClose,
@@ -225,6 +239,7 @@ function PanelChrome({
225
239
  }: {
226
240
  title: ReactNode
227
241
  subtitle?: ReactNode
242
+ badge?: ReactNode
228
243
  minimized?: boolean
229
244
  onMinimize?: () => void
230
245
  onClose?: () => void
@@ -234,7 +249,10 @@ function PanelChrome({
234
249
  return (
235
250
  <div className="hud-panel-chrome">
236
251
  <div className="hud-panel-chrome-heading">
237
- <div className="hud-panel-chrome-title">{title}</div>
252
+ <div className="hud-panel-chrome-title-row">
253
+ <div className="hud-panel-chrome-title">{title}</div>
254
+ {badge}
255
+ </div>
238
256
  {subtitle ? (
239
257
  <div className="hud-panel-chrome-subtitle">{subtitle}</div>
240
258
  ) : null}
@@ -305,13 +323,13 @@ function HandshakeSetup({
305
323
  onInstructionChange,
306
324
  blueprintDefined,
307
325
  awaitingAttach,
308
- attachBlockedBy,
326
+ nextAttachLabel,
309
327
  }: {
310
328
  instruction: string
311
329
  onInstructionChange: (value: string) => void
312
330
  blueprintDefined: boolean
313
331
  awaitingAttach: boolean
314
- attachBlockedBy: string | null
332
+ nextAttachLabel: string | null
315
333
  }) {
316
334
  return (
317
335
  <div className="hud-setup">
@@ -339,10 +357,10 @@ function HandshakeSetup({
339
357
  </section>
340
358
  <section className="hud-setup-section">
341
359
  <h2 className="hud-setup-heading">Start</h2>
342
- {attachBlockedBy ? (
360
+ {awaitingAttach && nextAttachLabel ? (
343
361
  <p>
344
- An LLM is already attached to {attachBlockedBy}. Stop that session
345
- before running <kbd>/inbase</kbd>.
362
+ <kbd>/inbase</kbd> attaches {nextAttachLabel} first. This session
363
+ stays in the queue.
346
364
  </p>
347
365
  ) : awaitingAttach ? (
348
366
  <p>
@@ -350,7 +368,7 @@ function HandshakeSetup({
350
368
  </p>
351
369
  ) : (
352
370
  <p>
353
- Starting from <kbd>/inbase</kbd>…
371
+ This window is attached. Starting from <kbd>/inbase</kbd>…
354
372
  </p>
355
373
  )}
356
374
  </section>
@@ -462,7 +480,7 @@ type SessionPanelProps = {
462
480
  intent: AgentIntent
463
481
  focused: boolean
464
482
  naming: boolean
465
- attachedSession?: AgentIntent | null
483
+ nextAttachSession?: AgentIntent | null
466
484
  onFocus: () => void
467
485
  onWorkflowAction: (
468
486
  sessionId: string,
@@ -476,7 +494,7 @@ function SessionPanel({
476
494
  intent,
477
495
  focused,
478
496
  naming,
479
- attachedSession = null,
497
+ nextAttachSession = null,
480
498
  onFocus,
481
499
  onWorkflowAction,
482
500
  onNavigateDiff,
@@ -563,11 +581,11 @@ function SessionPanel({
563
581
  const llmConnected = intent.awaitingAttach === false
564
582
  const showConnectedProgress =
565
583
  llmConnected && (askingBlueprint || sendingBlueprint || preparing)
566
- const attachBlockedBy =
584
+ const queuedBehind =
567
585
  intent.awaitingAttach &&
568
- attachedSession &&
569
- attachedSession.sessionId !== sessionId
570
- ? sessionLabel(attachedSession) || 'another session'
586
+ nextAttachSession &&
587
+ nextAttachSession.sessionId !== sessionId
588
+ ? sessionLabel(nextAttachSession) || 'a newer session'
571
589
  : null
572
590
 
573
591
  const act = (
@@ -584,6 +602,7 @@ function SessionPanel({
584
602
  }
585
603
  data-minimized={minimized}
586
604
  data-focused={focused}
605
+ data-attached={llmConnected}
587
606
  onPointerDown={onFocus}
588
607
  >
589
608
  <PanelChrome
@@ -598,6 +617,7 @@ function SessionPanel({
598
617
  : reviewTitle(intent.status)
599
618
  : undefined
600
619
  }
620
+ badge={<AttachStateBadge attached={llmConnected} />}
601
621
  minimized={minimized}
602
622
  onMinimize={() => setMinimized((current) => !current)}
603
623
  onClose={() => act('stop')}
@@ -637,20 +657,18 @@ function SessionPanel({
637
657
  onInstructionChange={updateInitialInstruction}
638
658
  blueprintDefined={blueprintIsDefined(intent)}
639
659
  awaitingAttach={Boolean(intent.awaitingAttach)}
640
- attachBlockedBy={attachBlockedBy || null}
660
+ nextAttachLabel={queuedBehind}
641
661
  />
642
662
  ) : intent.awaitingAttach ? (
643
- attachedSession &&
644
- attachedSession.sessionId !== sessionId ? (
663
+ queuedBehind ? (
645
664
  <p className="hud-mode-hint">
646
- An LLM is already attached to{' '}
647
- {sessionLabel(attachedSession) || 'another session'}. Stop that
648
- session before running <kbd>/inbase</kbd>.
665
+ <kbd>/inbase</kbd> attaches {queuedBehind} first. This session
666
+ stays in the queue.
649
667
  </p>
650
668
  ) : (
651
669
  <p className="hud-mode-hint">
652
670
  No LLM is attached. Open a Cursor chat and run{' '}
653
- <kbd>/inbase</kbd>. It connects to this focused session.
671
+ <kbd>/inbase</kbd>. It connects to the next waiting session.
654
672
  </p>
655
673
  )
656
674
  ) : null}
@@ -1207,7 +1225,7 @@ function explorerInstructions({
1207
1225
  {
1208
1226
  id: 'setup-session',
1209
1227
  keys: ['Setup LLM session'],
1210
- label: 'Open a session; /inbase connects the focused one',
1228
+ label: 'Open a session; /inbase attaches the newest waiting one',
1211
1229
  },
1212
1230
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
1213
1231
  {
@@ -1294,7 +1312,7 @@ function explorerInstructions({
1294
1312
  {
1295
1313
  id: 'setup-session',
1296
1314
  keys: ['Setup LLM session'],
1297
- label: 'Open a session; /inbase connects the focused one',
1315
+ label: 'Open a session; /inbase attaches the newest waiting one',
1298
1316
  },
1299
1317
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
1300
1318
  ...stop,
@@ -1318,6 +1336,7 @@ type HUDProps = {
1318
1336
  intent: AgentIntent
1319
1337
  intents?: AgentIntent[]
1320
1338
  focusedSessionId?: string | null
1339
+ nextAttachSessionId?: string | null
1321
1340
  onFocusSession?: (sessionId: string) => void
1322
1341
  onSetupSession?: () => Promise<unknown>
1323
1342
  onWorkflowAction: (
@@ -1383,6 +1402,7 @@ export function HUD({
1383
1402
  intent,
1384
1403
  intents,
1385
1404
  focusedSessionId = null,
1405
+ nextAttachSessionId = null,
1386
1406
  onFocusSession,
1387
1407
  onSetupSession,
1388
1408
  onWorkflowAction,
@@ -1444,8 +1464,10 @@ export function HUD({
1444
1464
  (item) => item.sessionId && isReviewingIntent(item.status),
1445
1465
  )
1446
1466
  const canStop = canStopSession(intent)
1447
- const attachedSession =
1448
- sessions.find((session) => session.awaitingAttach === false) ?? null
1467
+ const nextAttachSession =
1468
+ sessions.find((session) => session.sessionId === nextAttachSessionId) ??
1469
+ [...sessions].reverse().find((session) => session.awaitingAttach) ??
1470
+ null
1449
1471
  const [walkIntro, setWalkIntro] = useState(false)
1450
1472
  const walkIntroSeen = useRef(false)
1451
1473
  const [instructionsOpen, setInstructionsOpen] = useState(false)
@@ -1783,6 +1805,8 @@ export function HUD({
1783
1805
  {sessions.map((session) => {
1784
1806
  const active =
1785
1807
  session.sessionId === (focusedSessionId ?? intent.sessionId)
1808
+ const attached = session.awaitingAttach === false
1809
+ const label = sessionTabLabel(session, sessions)
1786
1810
  return (
1787
1811
  <button
1788
1812
  className="hud-button hud-session-tab"
@@ -1790,13 +1814,15 @@ export function HUD({
1790
1814
  role="tab"
1791
1815
  aria-selected={active}
1792
1816
  data-active={active}
1817
+ data-attached={attached}
1793
1818
  key={session.sessionId}
1794
- title={sessionTabLabel(session, sessions)}
1819
+ title={attached ? `${label} · Attached` : `${label} · Waiting`}
1795
1820
  onClick={() => {
1796
1821
  if (session.sessionId) onFocusSession?.(session.sessionId)
1797
1822
  }}
1798
1823
  >
1799
- {sessionTabLabel(session, sessions)}
1824
+ <span className="hud-attach-dot" aria-hidden="true" />
1825
+ <span className="hud-session-tab-label">{label}</span>
1800
1826
  </button>
1801
1827
  )
1802
1828
  })}
@@ -1807,7 +1833,7 @@ export function HUD({
1807
1833
  intent={intent}
1808
1834
  focused
1809
1835
  naming={naming}
1810
- attachedSession={attachedSession}
1836
+ nextAttachSession={nextAttachSession}
1811
1837
  onFocus={() => {
1812
1838
  if (intent.sessionId) onFocusSession?.(intent.sessionId)
1813
1839
  }}
@@ -17,6 +17,7 @@ import {
17
17
  inspectTargetFile,
18
18
  invokeStep,
19
19
  listSessionIntents,
20
+ nextAttachSessionId,
20
21
  readActiveSession,
21
22
  requestReplan,
22
23
  sendBlueprint,
@@ -142,6 +143,7 @@ function jsonFilePlugin(): Plugin {
142
143
  }
143
144
  sendJson(res, 200, {
144
145
  focusedSessionId: readActiveSession(dataDir),
146
+ nextAttachSessionId: nextAttachSessionId(dataDir),
145
147
  intents: listSessionIntents(dataDir, knownFileIds()),
146
148
  })
147
149
  return
package/bin/session.mjs CHANGED
@@ -179,7 +179,7 @@ export async function attachSession(args) {
179
179
  console.log(`VISUAL_CODER_SESSION ${manifest.sessionId}`)
180
180
  printAck('attached', manifest.name || manifest.sessionId)
181
181
  console.log(
182
- `VISUAL_CODER_ATTACHED Attached to the focused visualizer session ${manifest.name || manifest.sessionId} (${manifest.phase}). Use --session ${manifest.sessionId} for every later command. Run inbase wait-for-blueprint --session ${manifest.sessionId} to read the optional blueprint and instruction; it does not wait.`,
182
+ `VISUAL_CODER_ATTACHED Attached to the next waiting visualizer session ${manifest.name || manifest.sessionId} (${manifest.phase}). Use --session ${manifest.sessionId} for every later command. Run inbase wait-for-blueprint --session ${manifest.sessionId} to read the optional blueprint and instruction; it does not wait.`,
183
183
  )
184
184
  }
185
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jkwd/inbase",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A first-person 3D map of a codebase, with a visual coding workflow for Cursor.",
5
5
  "license": "MIT",
6
6
  "author": "Joris Kuijper",
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Attach this chat to the focused Inbase visualizer session
2
+ description: Attach this chat to the next waiting Inbase visualizer session
3
3
  ---
4
4
 
5
5
  The user invoked `/inbase`. This is how a chat joins the visualizer session that **Setup LLM session** already created.
6
6
 
7
7
  Do **not** ask for a session id. Do **not** run `inbase start-session`. Do **not** print the "direct chat interaction not allowed" message.
8
8
 
9
- 1. Attach to the focused visualizer session:
9
+ 1. Attach to the next waiting visualizer session (newest first; skip sessions that already have an LLM):
10
10
 
11
11
  ```bash
12
12
  npx inbase attach
@@ -57,7 +57,8 @@ If this chat is not yet attached, run:
57
57
  npx inbase attach
58
58
  ```
59
59
 
60
- That attaches this chat to the session currently focused in the map. No id is
60
+ That attaches this chat to the next waiting visualizer session (newest first).
61
+ Already-attached sessions are skipped. Window focus does not matter. No id is
61
62
  passed in; read `VISUAL_CODER_SESSION` from the output and use that
62
63
  `--session` value for every later command. Then continue from
63
64
  `wait-for-blueprint` below. Do **not** run `start-session`. Do **not** wait