@jkwd/inbase 0.1.9 → 0.1.11

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,4 +1,5 @@
1
1
  import { useEffect, useRef, useState, type ReactNode } from 'react'
2
+ import { persistInitialInstruction } from '../agentIntent'
2
3
  import { NameInput } from './NameInput'
3
4
  import { SelectionThumbnail } from '../scene/SelectionThumbnail'
4
5
  import {
@@ -7,6 +8,7 @@ import {
7
8
  type AgentIntent,
8
9
  type AgentIntentStatus,
9
10
  type AimedRelation,
11
+ type BranchChanges,
10
12
  type CodebaseGraph,
11
13
  type PatchImportAddition,
12
14
  type PatchSymbolAddition,
@@ -29,6 +31,20 @@ function reviewTitle(status: AgentIntentStatus) {
29
31
  return 'Visual workflow'
30
32
  }
31
33
 
34
+ function sessionLabel(intent: AgentIntent) {
35
+ return intent.name?.trim() || intent.feature?.trim() || ''
36
+ }
37
+
38
+ function sessionTabLabel(intent: AgentIntent, sessions: AgentIntent[]) {
39
+ const title = sessionLabel(intent) || reviewTitle(intent.status)
40
+ const same = sessions.filter(
41
+ (item) => (sessionLabel(item) || reviewTitle(item.status)) === title,
42
+ )
43
+ if (same.length < 2) return title
44
+ const id = intent.sessionId ?? ''
45
+ return `${title} · ${id.slice(-4)}`
46
+ }
47
+
32
48
  function fileBase(id: string) {
33
49
  return id.split('/').pop() ?? id
34
50
  }
@@ -200,18 +216,29 @@ function AddIntentRow({
200
216
 
201
217
  function PanelChrome({
202
218
  title,
219
+ subtitle,
203
220
  minimized = false,
204
221
  onMinimize,
205
222
  onClose,
223
+ closeLabel = 'Close',
224
+ closeReject = false,
206
225
  }: {
207
226
  title: ReactNode
227
+ subtitle?: ReactNode
208
228
  minimized?: boolean
209
229
  onMinimize?: () => void
210
230
  onClose?: () => void
231
+ closeLabel?: string
232
+ closeReject?: boolean
211
233
  }) {
212
234
  return (
213
235
  <div className="hud-panel-chrome">
214
- <div className="hud-panel-chrome-title">{title}</div>
236
+ <div className="hud-panel-chrome-heading">
237
+ <div className="hud-panel-chrome-title">{title}</div>
238
+ {subtitle ? (
239
+ <div className="hud-panel-chrome-subtitle">{subtitle}</div>
240
+ ) : null}
241
+ </div>
215
242
  <div className="hud-panel-controls">
216
243
  {onMinimize && (
217
244
  <button
@@ -225,9 +252,13 @@ function PanelChrome({
225
252
  )}
226
253
  {onClose && (
227
254
  <button
228
- className="hud-button hud-icon-button hud-panel-control"
255
+ className={
256
+ closeReject
257
+ ? 'hud-button hud-icon-button hud-panel-control hud-button-reject'
258
+ : 'hud-button hud-icon-button hud-panel-control'
259
+ }
229
260
  type="button"
230
- aria-label="Close"
261
+ aria-label={closeLabel}
231
262
  onClick={onClose}
232
263
  >
233
264
  ×
@@ -238,10 +269,200 @@ function PanelChrome({
238
269
  )
239
270
  }
240
271
 
272
+ function InitialInstructionField({
273
+ value,
274
+ onChange,
275
+ }: {
276
+ value: string
277
+ onChange: (value: string) => void
278
+ }) {
279
+ return (
280
+ <label className="hud-instruction">
281
+ <textarea
282
+ value={value}
283
+ maxLength={4000}
284
+ rows={4}
285
+ placeholder="What should the LLM build?"
286
+ onChange={(event) => onChange(event.target.value)}
287
+ onKeyDown={(event) => event.stopPropagation()}
288
+ />
289
+ </label>
290
+ )
291
+ }
292
+
293
+ function blueprintIsDefined(intent: AgentIntent) {
294
+ return (
295
+ (intent.userCreatedBlocks?.length ?? 0) > 0 ||
296
+ (intent.userCreatedIslands?.length ?? 0) > 0 ||
297
+ (intent.blueprintFunctions?.length ?? 0) > 0 ||
298
+ (intent.blueprintVariables?.length ?? 0) > 0 ||
299
+ (intent.blueprintImports?.length ?? 0) > 0
300
+ )
301
+ }
302
+
303
+ function HandshakeSetup({
304
+ instruction,
305
+ onInstructionChange,
306
+ blueprintDefined,
307
+ awaitingAttach,
308
+ attachBlockedBy,
309
+ }: {
310
+ instruction: string
311
+ onInstructionChange: (value: string) => void
312
+ blueprintDefined: boolean
313
+ awaitingAttach: boolean
314
+ attachBlockedBy: string | null
315
+ }) {
316
+ return (
317
+ <div className="hud-setup">
318
+ <section className="hud-setup-section">
319
+ <h2 className="hud-setup-heading">Instructions</h2>
320
+ <InitialInstructionField
321
+ value={instruction}
322
+ onChange={onInstructionChange}
323
+ />
324
+ </section>
325
+ <section className="hud-setup-section">
326
+ <h2 className="hud-setup-heading">
327
+ Blueprint
328
+ <span
329
+ className="hud-setup-tag"
330
+ data-ready={blueprintDefined ? 'true' : 'false'}
331
+ >
332
+ {blueprintDefined ? 'blueprint defined' : 'no blueprint'}
333
+ </span>
334
+ </h2>
335
+ <p>
336
+ Press <kbd>Space</kbd> for a file and <kbd>B</kbd> for an island.
337
+ Optional.
338
+ </p>
339
+ </section>
340
+ <section className="hud-setup-section">
341
+ <h2 className="hud-setup-heading">Start</h2>
342
+ {attachBlockedBy ? (
343
+ <p>
344
+ An LLM is already attached to {attachBlockedBy}. Stop that session
345
+ before running <kbd>/inbase</kbd>.
346
+ </p>
347
+ ) : awaitingAttach ? (
348
+ <p>
349
+ Run <kbd>/inbase</kbd> in a Cursor chat to connect and start.
350
+ </p>
351
+ ) : (
352
+ <p>
353
+ Starting from <kbd>/inbase</kbd>…
354
+ </p>
355
+ )}
356
+ </section>
357
+ </div>
358
+ )
359
+ }
360
+
361
+ function sessionLiveStatus(intent: AgentIntent) {
362
+ const ack = intent.lastAck
363
+ const kind = ack?.kind
364
+ const detail = ack?.detail?.trim() || ''
365
+
366
+ if (intent.status === 'finished' || kind === 'finished') {
367
+ return { text: 'Finished', busy: false }
368
+ }
369
+ if (kind === 'stopped' || intent.status === 'rejected') {
370
+ return { text: 'Stopped', busy: false }
371
+ }
372
+ if (kind === 'timeout') {
373
+ return { text: 'LLM wait timed out', busy: false }
374
+ }
375
+ if (intent.awaitingAttach) {
376
+ return { text: 'Waiting for /inbase in Cursor', busy: true }
377
+ }
378
+ if (kind === 'execute') {
379
+ return { text: `LLM received ${detail}`, busy: true }
380
+ }
381
+ if (kind === 'invoke') {
382
+ return { text: `Sent ${detail} — waiting for LLM`, busy: true }
383
+ }
384
+ if (kind === 'replan') {
385
+ return { text: 'LLM received a new instruction', busy: true }
386
+ }
387
+ if (intent.status === 'working') {
388
+ return {
389
+ text: intent.reason
390
+ ? `LLM is working on ${intent.reason}`
391
+ : 'LLM is working',
392
+ busy: true,
393
+ }
394
+ }
395
+ if (intent.status === 'replanning') {
396
+ return { text: 'LLM is revising the plan', busy: true }
397
+ }
398
+ if (intent.status === 'preparing' || kind === 'blueprint') {
399
+ return { text: 'LLM is drafting the plan', busy: true }
400
+ }
401
+ if (kind === 'plan' || intent.status === 'planned') {
402
+ return intent.listening
403
+ ? { text: 'LLM is listening — run the next step', busy: false }
404
+ : { text: 'Plan ready', busy: false }
405
+ }
406
+ if (intent.status === 'pending') {
407
+ return intent.listening
408
+ ? { text: 'LLM is listening — accept or send an instruction', busy: false }
409
+ : { text: 'Review this step', busy: false }
410
+ }
411
+ if (
412
+ kind === 'attached' ||
413
+ intent.status === 'blueprint' ||
414
+ intent.status === 'blueprint_ask'
415
+ ) {
416
+ return { text: 'LLM attached', busy: true }
417
+ }
418
+ if (intent.llmIdle) {
419
+ return { text: 'LLM is idle', busy: false }
420
+ }
421
+ return { text: 'LLM connected', busy: true }
422
+ }
423
+
424
+ function LiveStatus({
425
+ intent,
426
+ showStop = false,
427
+ onStop,
428
+ }: {
429
+ intent: AgentIntent
430
+ showStop?: boolean
431
+ onStop?: () => void
432
+ }) {
433
+ const status = sessionLiveStatus(intent)
434
+ const [flash, setFlash] = useState(false)
435
+ const lastAt = intent.lastAck?.at
436
+
437
+ useEffect(() => {
438
+ if (!lastAt) return
439
+ setFlash(true)
440
+ const timer = window.setTimeout(() => setFlash(false), 700)
441
+ return () => window.clearTimeout(timer)
442
+ }, [lastAt])
443
+
444
+ return (
445
+ <div className="hud-live" data-busy={status.busy} data-flash={flash}>
446
+ {status.busy ? <span className="hud-spinner" aria-hidden="true" /> : null}
447
+ <span>{status.text}</span>
448
+ {showStop && onStop ? (
449
+ <button
450
+ className="hud-button hud-button-reject"
451
+ type="button"
452
+ onClick={onStop}
453
+ >
454
+ Stop
455
+ </button>
456
+ ) : null}
457
+ </div>
458
+ )
459
+ }
460
+
241
461
  type SessionPanelProps = {
242
462
  intent: AgentIntent
243
463
  focused: boolean
244
464
  naming: boolean
465
+ attachedSession?: AgentIntent | null
245
466
  onFocus: () => void
246
467
  onWorkflowAction: (
247
468
  sessionId: string,
@@ -255,16 +476,21 @@ function SessionPanel({
255
476
  intent,
256
477
  focused,
257
478
  naming,
479
+ attachedSession = null,
258
480
  onFocus,
259
481
  onWorkflowAction,
260
482
  onNavigateDiff,
261
483
  }: SessionPanelProps) {
262
484
  const [minimized, setMinimized] = useState(false)
263
485
  const [instruction, setInstruction] = useState('')
486
+ const [initialInstruction, setInitialInstruction] = useState(
487
+ () => intent.initialInstruction ?? '',
488
+ )
264
489
  const sessionId = intent.sessionId
265
490
  const pending = intent.status === 'pending' && intent.isActiveDiff
266
491
  const askingBlueprint = intent.status === 'blueprint_ask'
267
- const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
492
+ const sendingBlueprint = intent.status === 'blueprint'
493
+ const canPlace = Boolean(intent.creationMode)
268
494
  const preparing = intent.status === 'preparing'
269
495
  const planReady = intent.status === 'planned'
270
496
  const working = intent.status === 'working' || intent.status === 'replanning'
@@ -286,7 +512,7 @@ function SessionPanel({
286
512
  const addedImports = intent.addedImports ?? []
287
513
  const changedFunctions = intent.changedFunctions ?? []
288
514
  const changedVariables = intent.changedVariables ?? []
289
- const doneSteps = new Set(
515
+ const acceptedSteps = new Set(
290
516
  intent.status === 'finished'
291
517
  ? intent.steps.map((step) => step.index)
292
518
  : intent.chain
@@ -297,29 +523,53 @@ function SessionPanel({
297
523
  .map((entry) => entry.step),
298
524
  )
299
525
  if (intent.status === 'approved' && typeof intent.step === 'number') {
300
- doneSteps.add(intent.step)
301
- }
302
- if (pending && typeof intent.step === 'number' && !lastStep) {
303
- doneSteps.add(intent.step)
526
+ acceptedSteps.add(intent.step)
304
527
  }
305
- const nextStep =
306
- intent.status === 'finished'
307
- ? null
308
- : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
309
- const canRunNext =
310
- Boolean(nextStep) && (planReady || pending) && !working
311
- const canComplete = pending && lastStep
528
+ const proposalStep =
529
+ pending && typeof intent.step === 'number' ? intent.step : null
530
+ const processingStep =
531
+ working && typeof intent.step === 'number' ? intent.step : null
532
+ const invokeStep =
533
+ planReady && !working
534
+ ? (intent.steps.find((step) => !acceptedSteps.has(step.index)) ?? null)
535
+ : null
536
+ const canRunNext = stepByStep && Boolean(invokeStep)
537
+ const canAcceptProposal = proposalStep !== null
312
538
  const panelDone =
313
539
  intent.status === 'finished' ||
314
540
  intent.status === 'approved' ||
315
- doneSteps.size > 0
541
+ acceptedSteps.size > 0 ||
542
+ canAcceptProposal
316
543
 
317
544
  useEffect(() => {
318
545
  setInstruction('')
319
546
  }, [intent.diffId])
320
547
 
548
+ useEffect(() => {
549
+ setInitialInstruction(intent.initialInstruction ?? '')
550
+ }, [sessionId])
551
+
321
552
  if (!sessionId || !isReviewingIntent(intent.status)) return null
322
553
 
554
+ const updateInitialInstruction = (value: string) => {
555
+ setInitialInstruction(value)
556
+ persistInitialInstruction(sessionId, value)
557
+ }
558
+
559
+ const showInitialInstruction =
560
+ Boolean(intent.awaitingAttach) &&
561
+ (askingBlueprint || sendingBlueprint || preparing)
562
+ const handshakeSetup = showInitialInstruction
563
+ const llmConnected = intent.awaitingAttach === false
564
+ const showConnectedProgress =
565
+ llmConnected && (askingBlueprint || sendingBlueprint || preparing)
566
+ const attachBlockedBy =
567
+ intent.awaitingAttach &&
568
+ attachedSession &&
569
+ attachedSession.sessionId !== sessionId
570
+ ? sessionLabel(attachedSession) || 'another session'
571
+ : null
572
+
323
573
  const act = (
324
574
  action: WorkflowAction,
325
575
  options?: { instruction?: string; step?: number; stepByStep?: boolean },
@@ -337,12 +587,30 @@ function SessionPanel({
337
587
  onPointerDown={onFocus}
338
588
  >
339
589
  <PanelChrome
340
- title={reviewTitle(intent.status)}
590
+ title={
591
+ sessionLabel(intent) ||
592
+ (showConnectedProgress ? 'LLM connected' : reviewTitle(intent.status))
593
+ }
594
+ subtitle={
595
+ sessionLabel(intent)
596
+ ? showConnectedProgress
597
+ ? 'LLM connected'
598
+ : reviewTitle(intent.status)
599
+ : undefined
600
+ }
341
601
  minimized={minimized}
342
602
  onMinimize={() => setMinimized((current) => !current)}
603
+ onClose={() => act('stop')}
604
+ closeLabel="Stop session"
605
+ closeReject
343
606
  />
344
607
  {!minimized && (
345
608
  <>
609
+ <LiveStatus
610
+ intent={intent}
611
+ showStop={showConnectedProgress || working}
612
+ onStop={() => act('stop')}
613
+ />
346
614
  <label className="hud-mode-switch">
347
615
  <span>Step by step</span>
348
616
  <button
@@ -358,22 +626,48 @@ function SessionPanel({
358
626
  {!stepByStep && (
359
627
  <p className="hud-mode-hint">
360
628
  LLM implements the full plan. You can still walk the diffs, then
361
- Complete.
629
+ Accept proposal.
362
630
  </p>
363
631
  )}
364
- {intent.feature && <p className="hud-feature">{intent.feature}</p>}
365
- {askingBlueprint ? (
632
+ {handshakeSetup ? (
633
+ <HandshakeSetup
634
+ instruction={initialInstruction}
635
+ onInstructionChange={updateInitialInstruction}
636
+ blueprintDefined={blueprintIsDefined(intent)}
637
+ awaitingAttach={Boolean(intent.awaitingAttach)}
638
+ attachBlockedBy={attachBlockedBy || null}
639
+ />
640
+ ) : intent.awaitingAttach ? (
641
+ attachedSession &&
642
+ attachedSession.sessionId !== sessionId ? (
643
+ <p className="hud-mode-hint">
644
+ An LLM is already attached to{' '}
645
+ {sessionLabel(attachedSession) || 'another session'}. Stop that
646
+ session before running <kbd>/inbase</kbd>.
647
+ </p>
648
+ ) : (
649
+ <p className="hud-mode-hint">
650
+ No LLM is attached. Open a Cursor chat and run{' '}
651
+ <kbd>/inbase</kbd>. It connects to this focused session.
652
+ </p>
653
+ )
654
+ ) : null}
655
+ {intent.feature &&
656
+ !handshakeSetup &&
657
+ intent.feature.trim() !== sessionLabel(intent) && (
658
+ <p className="hud-feature">{intent.feature}</p>
659
+ )}
660
+ {askingBlueprint && !handshakeSetup && !showConnectedProgress ? (
366
661
  <>
367
662
  <p>
368
- {intent.canEnterBlueprint
369
- ? 'Place files and folders for this chat, then send them as a blueprint for the LLM?'
370
- : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
663
+ Place files and folders for this chat, then send them as a
664
+ blueprint for the LLM? You can still add files and islands on
665
+ later steps.
371
666
  </p>
372
667
  <div className="hud-decide">
373
668
  <button
374
669
  className="hud-button hud-button-approve"
375
670
  type="button"
376
- disabled={!intent.canEnterBlueprint}
377
671
  onClick={() => act('blueprint_yes')}
378
672
  >
379
673
  Create blueprint
@@ -394,113 +688,71 @@ function SessionPanel({
394
688
  </button>
395
689
  </div>
396
690
  </>
397
- ) : creatingBlueprint ? (
398
- <>
399
- <p>
400
- Walk the map, press <kbd>Space</kbd> for a file and{' '}
401
- <kbd>B</kbd> for an island. Send the blueprint when the layout
402
- is ready.
403
- </p>
404
- <div className="hud-decide">
405
- <button
406
- className="hud-button hud-button-approve"
407
- type="button"
408
- disabled={naming}
409
- onClick={() => act('blueprint_send')}
410
- >
411
- Send blueprint
412
- </button>
413
- <button
414
- className="hud-button hud-button-reject"
415
- type="button"
416
- onClick={() => act('stop')}
417
- >
418
- Stop
419
- </button>
420
- </div>
421
- </>
422
- ) : intent.status === 'finished' ? (
691
+ ) : handshakeSetup ? null : intent.status === 'finished' ? (
423
692
  <p>All plan steps were applied.</p>
424
- ) : preparing ? (
425
- <div className="hud-working">
426
- <span className="hud-spinner" aria-hidden="true" />
427
- <span>LLM preparing…</span>
428
- <button
429
- className="hud-button hud-button-reject"
430
- type="button"
431
- onClick={() => act('stop')}
432
- >
433
- Stop
434
- </button>
435
- </div>
436
- ) : working ? (
437
- <div className="hud-working">
438
- <span className="hud-spinner" aria-hidden="true" />
439
- <span>
440
- {intent.status === 'replanning'
441
- ? 'Updating the remaining plan from your instruction…'
442
- : intent.stalledWait
443
- ? 'The LLM is still waiting on this step…'
444
- : `Implementing ${stepLabel.toLowerCase()}…`}
445
- </span>
446
- <button
447
- className="hud-button hud-button-reject"
448
- type="button"
449
- onClick={() => act('stop')}
450
- >
451
- Stop
452
- </button>
453
- </div>
454
- ) : (
693
+ ) : showConnectedProgress || preparing ? null : (
455
694
  <p>
456
695
  {stepLabel}
457
696
  {intent.reason ? ` · ${intent.reason}` : ''}
458
697
  </p>
459
698
  )}
460
- {!askingBlueprint && !creatingBlueprint && (
699
+ {!askingBlueprint && !sendingBlueprint && (
461
700
  <>
701
+ {canPlace && !intent.working && (
702
+ <p>
703
+ <kbd>Space</kbd> places a file, <kbd>B</kbd> an island for
704
+ this session.
705
+ </p>
706
+ )}
462
707
  {intent.steps?.length > 0 && (
463
708
  <ol className="hud-steps">
464
- {intent.steps.map((step) => (
465
- <li
466
- key={step.index}
467
- data-current={
468
- intent.status !== 'finished' &&
469
- step.index === intent.step &&
470
- !doneSteps.has(step.index)
471
- }
472
- data-done={doneSteps.has(step.index)}
473
- data-next={
474
- nextStep?.index === step.index &&
475
- !doneSteps.has(step.index)
476
- }
477
- >
478
- <span className="hud-step-index">{step.index}.</span>
479
- <span className="hud-step-main">
480
- <span className="hud-step-title">{step.title}</span>
481
- {((stepByStep &&
482
- canRunNext &&
483
- nextStep?.index === step.index) ||
484
- (canComplete && step.index === intent.step)) && (
485
- <button
486
- className="hud-button hud-button-approve hud-run-step"
487
- type="button"
488
- onClick={() =>
489
- canComplete && step.index === intent.step
490
- ? act('continue')
491
- : act('invoke', {
492
- step: step.index,
493
- })
494
- }
495
- >
496
- {canComplete && step.index === intent.step
497
- ? 'Complete'
498
- : 'Run step'}
499
- </button>
500
- )}
501
- </span>
502
- </li>
503
- ))}
709
+ {intent.steps.map((step) => {
710
+ const proposed = proposalStep === step.index
711
+ const processing = processingStep === step.index
712
+ const accepted = acceptedSteps.has(step.index)
713
+ const creating = processing && !proposed
714
+ const showStepAction =
715
+ creating ||
716
+ (canRunNext && invokeStep?.index === step.index) ||
717
+ (canAcceptProposal && proposed)
718
+ return (
719
+ <li
720
+ key={step.index}
721
+ data-done={accepted || proposed}
722
+ data-active={processing || proposed}
723
+ >
724
+ <span className="hud-step-index">{step.index}.</span>
725
+ <span className="hud-step-main">
726
+ <span className="hud-step-title">{step.title}</span>
727
+ {showStepAction && (
728
+ <button
729
+ className="hud-button hud-button-approve hud-run-step"
730
+ type="button"
731
+ disabled={creating}
732
+ aria-busy={creating}
733
+ onClick={() =>
734
+ proposed
735
+ ? lastStep
736
+ ? act('continue')
737
+ : act('invoke', {
738
+ step: step.index + 1,
739
+ })
740
+ : act('invoke', {
741
+ step: step.index,
742
+ })
743
+ }
744
+ >
745
+ {proposed
746
+ ? 'Accept proposal'
747
+ : creating
748
+ ? 'Creating proposal…'
749
+ : 'Create proposal'}
750
+ </button>
751
+ )}
752
+ </span>
753
+ </li>
754
+ )
755
+ })}
504
756
  </ol>
505
757
  )}
506
758
  {intent.chain.length > 0 && (
@@ -658,18 +910,10 @@ function SessionPanel({
658
910
  rows={3}
659
911
  placeholder="Describe what should change in the next diff…"
660
912
  onChange={(event) => setInstruction(event.target.value)}
913
+ onKeyDown={(event) => event.stopPropagation()}
661
914
  />
662
915
  </label>
663
916
  <div className="hud-decide">
664
- {lastStep && (
665
- <button
666
- className="hud-button hud-button-approve"
667
- type="button"
668
- onClick={() => act('continue')}
669
- >
670
- Complete
671
- </button>
672
- )}
673
917
  <button
674
918
  className="hud-button hud-button-extend"
675
919
  type="button"
@@ -698,6 +942,131 @@ function SessionPanel({
698
942
  )
699
943
  }
700
944
 
945
+ function BranchChangesPanel({
946
+ changes,
947
+ }: {
948
+ changes: BranchChanges
949
+ }) {
950
+ const [minimized, setMinimized] = useState(false)
951
+ const addedFunctions = changes.addedFunctions ?? []
952
+ const addedVariables = changes.addedVariables ?? []
953
+ const addedImports = changes.addedImports ?? []
954
+ const changedFunctions = changes.changedFunctions ?? []
955
+ const changedVariables = changes.changedVariables ?? []
956
+ const subtitle =
957
+ changes.branch && changes.base
958
+ ? `${changes.branch} vs ${changes.base}`
959
+ : changes.branch
960
+ ? changes.branch
961
+ : null
962
+ const hasContent =
963
+ changes.files.length > 0 ||
964
+ (changes.createFolders ?? []).length > 0 ||
965
+ changes.creates.length > 0 ||
966
+ changes.deletes.length > 0 ||
967
+ addedFunctions.length > 0 ||
968
+ addedVariables.length > 0 ||
969
+ addedImports.length > 0 ||
970
+ changedFunctions.length > 0 ||
971
+ changedVariables.length > 0 ||
972
+ (changes.imports ?? []).length > 0
973
+
974
+ return (
975
+ <aside
976
+ className="hud-panel hud-panel-planned hud-panel-done"
977
+ data-minimized={minimized}
978
+ >
979
+ <PanelChrome
980
+ title="Branch changes"
981
+ minimized={minimized}
982
+ onMinimize={() => setMinimized((current) => !current)}
983
+ />
984
+ {!minimized && (
985
+ <>
986
+ {subtitle && <p className="hud-feature">{subtitle}</p>}
987
+ {!hasContent ? (
988
+ <p>No file changes on this branch.</p>
989
+ ) : (
990
+ <MutationFold hasContent>
991
+ {changes.files.length > 0 && (
992
+ <>
993
+ <div className="hud-section-title hud-section-title-edit">
994
+ Changed
995
+ </div>
996
+ <ul>
997
+ {changes.files.map((id) => (
998
+ <li className="hud-file-edit" key={id}>
999
+ {id}
1000
+ </li>
1001
+ ))}
1002
+ </ul>
1003
+ </>
1004
+ )}
1005
+ {(changes.createFolders ?? []).length > 0 && (
1006
+ <>
1007
+ <div className="hud-section-title hud-section-title-add">
1008
+ Added islands
1009
+ </div>
1010
+ <ul>
1011
+ {changes.createFolders.map((id) => (
1012
+ <li className="hud-file-add" key={id}>
1013
+ {id}/
1014
+ </li>
1015
+ ))}
1016
+ </ul>
1017
+ </>
1018
+ )}
1019
+ {changes.creates.length > 0 && (
1020
+ <>
1021
+ <div className="hud-section-title hud-section-title-add">
1022
+ Added
1023
+ </div>
1024
+ <ul>
1025
+ {changes.creates.map((id) => (
1026
+ <li className="hud-file-add" key={id}>
1027
+ {id}
1028
+ </li>
1029
+ ))}
1030
+ </ul>
1031
+ </>
1032
+ )}
1033
+ {changes.deletes.length > 0 && (
1034
+ <>
1035
+ <div className="hud-section-title hud-section-title-remove">
1036
+ Removed
1037
+ </div>
1038
+ <ul>
1039
+ {changes.deletes.map((id) => (
1040
+ <li className="hud-file-remove" key={id}>
1041
+ {id}
1042
+ </li>
1043
+ ))}
1044
+ </ul>
1045
+ </>
1046
+ )}
1047
+ <PatchSymbolChanges
1048
+ title="Functions"
1049
+ added={addedFunctions}
1050
+ changed={changedFunctions}
1051
+ />
1052
+ <PatchSymbolChanges
1053
+ title="Vars"
1054
+ added={addedVariables}
1055
+ changed={changedVariables}
1056
+ />
1057
+ <PanelList
1058
+ title="Imports"
1059
+ items={importLabels(addedImports)}
1060
+ tone="add"
1061
+ />
1062
+ </MutationFold>
1063
+ )}
1064
+ </>
1065
+ )}
1066
+ </aside>
1067
+ )
1068
+ }
1069
+
701
1070
  type ExplorerInstruction = {
702
1071
  id: string
703
1072
  keys: string[]
@@ -732,7 +1101,7 @@ function InstructionList({ items }: { items: ExplorerInstruction[] }) {
732
1101
  }
733
1102
 
734
1103
  function explorerInstructions({
735
- creatingBlueprint,
1104
+ canPlace,
736
1105
  hasChangeSet,
737
1106
  changePathsOnly,
738
1107
  selectedUserCreated,
@@ -741,8 +1110,10 @@ function explorerInstructions({
741
1110
  importedBy,
742
1111
  canStop,
743
1112
  sessionCount,
1113
+ showBranchChanges,
1114
+ canShowBranchChanges,
744
1115
  }: {
745
- creatingBlueprint: boolean
1116
+ canPlace: boolean
746
1117
  hasChangeSet: boolean
747
1118
  changePathsOnly: boolean
748
1119
  selectedUserCreated: boolean
@@ -751,9 +1122,11 @@ function explorerInstructions({
751
1122
  importedBy: boolean
752
1123
  canStop: boolean
753
1124
  sessionCount: number
1125
+ showBranchChanges: boolean
1126
+ canShowBranchChanges: boolean
754
1127
  }): ExplorerInstructionSection[] {
755
1128
  const backspace: ExplorerInstruction[] =
756
- selectedUserCreated && creatingBlueprint
1129
+ selectedUserCreated && canPlace
757
1130
  ? [{ id: 'backspace', keys: ['Backspace'], label: 'Delete' }]
758
1131
  : []
759
1132
  const info: ExplorerInstruction[] = [
@@ -771,6 +1144,17 @@ function explorerInstructions({
771
1144
  keys: ['K'],
772
1145
  label: importedBy ? 'Show imports' : 'Show imported by',
773
1146
  }
1147
+ const branch: ExplorerInstruction[] = canShowBranchChanges
1148
+ ? [
1149
+ {
1150
+ id: 'branch-changes',
1151
+ keys: ['G'],
1152
+ label: showBranchChanges
1153
+ ? 'Hide branch changes'
1154
+ : 'Show branch changes',
1155
+ },
1156
+ ]
1157
+ : []
774
1158
  const stop: ExplorerInstruction[] = canStop
775
1159
  ? [
776
1160
  {
@@ -796,7 +1180,7 @@ function explorerInstructions({
796
1180
  { id: 'wasd', keys: ['W', 'A', 'S', 'D'], label: 'Walk' },
797
1181
  { id: 'mouse-look', keys: ['Mouse'], label: 'Look around' },
798
1182
  { id: 'shift', keys: ['Shift'], label: 'Sprint' },
799
- ...(creatingBlueprint
1183
+ ...(canPlace
800
1184
  ? [
801
1185
  { id: 'space', keys: ['Space'], label: 'Place file' },
802
1186
  { id: 'b-island', keys: ['B'], label: 'Place island' },
@@ -811,11 +1195,17 @@ function explorerInstructions({
811
1195
  { id: 'aim-line', keys: ['Click'], label: 'Aim a line to fly' },
812
1196
  ...info,
813
1197
  imported,
1198
+ ...branch,
814
1199
  {
815
1200
  id: 'update-model',
816
1201
  keys: ['Update model'],
817
1202
  label: 'Rescan and rebuild the map',
818
1203
  },
1204
+ {
1205
+ id: 'setup-session',
1206
+ keys: ['Setup LLM session'],
1207
+ label: 'Open a session; /inbase connects the focused one',
1208
+ },
819
1209
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
820
1210
  {
821
1211
  id: 'release',
@@ -856,7 +1246,7 @@ function explorerInstructions({
856
1246
  keys: ['Click'],
857
1247
  label: 'An island for its files',
858
1248
  },
859
- ...(creatingBlueprint
1249
+ ...(canPlace
860
1250
  ? [
861
1251
  { id: 'select-island', keys: ['Click'], label: 'Select an island' },
862
1252
  {
@@ -892,11 +1282,17 @@ function explorerInstructions({
892
1282
  ...info,
893
1283
  thumbnail,
894
1284
  imported,
1285
+ ...branch,
895
1286
  {
896
1287
  id: 'update-model',
897
1288
  keys: ['Update model'],
898
1289
  label: 'Rescan and rebuild the map',
899
1290
  },
1291
+ {
1292
+ id: 'setup-session',
1293
+ keys: ['Setup LLM session'],
1294
+ label: 'Open a session; /inbase connects the focused one',
1295
+ },
900
1296
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
901
1297
  ...stop,
902
1298
  ],
@@ -920,6 +1316,7 @@ type HUDProps = {
920
1316
  intents?: AgentIntent[]
921
1317
  focusedSessionId?: string | null
922
1318
  onFocusSession?: (sessionId: string) => void
1319
+ onSetupSession?: () => Promise<unknown>
923
1320
  onWorkflowAction: (
924
1321
  sessionId: string,
925
1322
  action: WorkflowAction,
@@ -930,6 +1327,11 @@ type HUDProps = {
930
1327
  onWalk: () => void
931
1328
  followLook: boolean
932
1329
  onToggleFollowLook: () => void
1330
+ showBranchChanges?: boolean
1331
+ branchChanges?: BranchChanges
1332
+ canShowBranchChanges?: boolean
1333
+ llmMakingChanges?: boolean
1334
+ onToggleShowBranchChanges?: () => void
933
1335
  onUpdateModel: () => void
934
1336
  updatingModel?: boolean
935
1337
  importedBy: boolean
@@ -979,12 +1381,18 @@ export function HUD({
979
1381
  intents,
980
1382
  focusedSessionId = null,
981
1383
  onFocusSession,
1384
+ onSetupSession,
982
1385
  onWorkflowAction,
983
1386
  onNavigateDiff,
984
1387
  onOpenMap,
985
1388
  onWalk,
986
1389
  followLook,
987
1390
  onToggleFollowLook,
1391
+ showBranchChanges = false,
1392
+ branchChanges,
1393
+ canShowBranchChanges = false,
1394
+ llmMakingChanges = false,
1395
+ onToggleShowBranchChanges,
988
1396
  onUpdateModel,
989
1397
  updatingModel = false,
990
1398
  importedBy,
@@ -1033,24 +1441,27 @@ export function HUD({
1033
1441
  (item) => item.sessionId && isReviewingIntent(item.status),
1034
1442
  )
1035
1443
  const canStop = canStopSession(intent)
1444
+ const attachedSession =
1445
+ sessions.find((session) => session.awaitingAttach === false) ?? null
1036
1446
  const [walkIntro, setWalkIntro] = useState(false)
1037
1447
  const walkIntroSeen = useRef(false)
1038
1448
  const [instructionsOpen, setInstructionsOpen] = useState(false)
1449
+ const [setupError, setSetupError] = useState<string | null>(null)
1450
+ const [setupBusy, setSetupBusy] = useState(false)
1039
1451
  const [infoVisible, setInfoVisible] = useState(false)
1040
1452
  const [infoMinimized, setInfoMinimized] = useState(false)
1041
1453
  const [thumbnailVisible, setThumbnailVisible] = useState(true)
1042
1454
  const [thumbnailMinimized, setThumbnailMinimized] = useState(false)
1043
1455
  const [thumbnailMaximized, setThumbnailMaximized] = useState(false)
1044
1456
  const infoPanelRef = useRef<HTMLDivElement>(null)
1045
- const creatingBlueprint = sessions.some(
1046
- (item) => item.creationMode || item.status === 'blueprint',
1047
- )
1048
- const previewing = intent.preview
1049
- const addedFunctions = intent.addedFunctions ?? []
1050
- const addedVariables = intent.addedVariables ?? []
1051
- const addedImports = intent.addedImports ?? []
1052
- const changedFunctions = intent.changedFunctions ?? []
1053
- const changedVariables = intent.changedVariables ?? []
1457
+ const canPlace = Boolean(intent.creationMode)
1458
+ const overlay = showBranchChanges && branchChanges ? branchChanges : intent
1459
+ const previewing = intent.preview || showBranchChanges
1460
+ const addedFunctions = overlay.addedFunctions ?? []
1461
+ const addedVariables = overlay.addedVariables ?? []
1462
+ const addedImports = overlay.addedImports ?? []
1463
+ const changedFunctions = overlay.changedFunctions ?? []
1464
+ const changedVariables = overlay.changedVariables ?? []
1054
1465
  const selectedAddedFunctions = selected
1055
1466
  ? addedFunctions.filter((item) => item.file === selected.id)
1056
1467
  : []
@@ -1111,7 +1522,7 @@ export function HUD({
1111
1522
  (item) => !selected?.imports.includes(item.from),
1112
1523
  )
1113
1524
  const canEditBlueprint =
1114
- creatingBlueprint && Boolean(selected) && !selected?.id.startsWith('draft:')
1525
+ canPlace && Boolean(selected) && !selected?.id.startsWith('draft:')
1115
1526
  const canInspectFile = (fileId: string, userCreated = false) =>
1116
1527
  Boolean(onInspectFile) &&
1117
1528
  !fileId.startsWith('draft:') &&
@@ -1268,7 +1679,7 @@ export function HUD({
1268
1679
  }, [instructionsOpen])
1269
1680
 
1270
1681
  const instructionSections = explorerInstructions({
1271
- creatingBlueprint,
1682
+ canPlace,
1272
1683
  hasChangeSet,
1273
1684
  changePathsOnly,
1274
1685
  selectedUserCreated: Boolean(selected?.userCreated),
@@ -1277,6 +1688,8 @@ export function HUD({
1277
1688
  importedBy,
1278
1689
  canStop,
1279
1690
  sessionCount: sessions.length,
1691
+ showBranchChanges,
1692
+ canShowBranchChanges,
1280
1693
  })
1281
1694
  const currentInstructionView: InstructionView = mapping
1282
1695
  ? thumbnailMaximized
@@ -1298,7 +1711,7 @@ export function HUD({
1298
1711
  <p>
1299
1712
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
1300
1713
  <kbd>Shift</kbd> sprint
1301
- {creatingBlueprint ? (
1714
+ {canPlace ? (
1302
1715
  <>
1303
1716
  , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
1304
1717
  </>
@@ -1360,21 +1773,48 @@ export function HUD({
1360
1773
  {selected && <div className="hud-chip">{selected.path}</div>}
1361
1774
  </div>
1362
1775
 
1363
- {sessions.length > 0 && (
1776
+ {(sessions.length > 0 || showBranchChanges) && (
1364
1777
  <div className="hud-left-stack">
1365
- {sessions.map((session) => (
1778
+ {sessions.length > 1 && (
1779
+ <div className="hud-session-tabs" role="tablist" aria-label="LLM sessions">
1780
+ {sessions.map((session) => {
1781
+ const active =
1782
+ session.sessionId === (focusedSessionId ?? intent.sessionId)
1783
+ return (
1784
+ <button
1785
+ className="hud-button hud-session-tab"
1786
+ type="button"
1787
+ role="tab"
1788
+ aria-selected={active}
1789
+ data-active={active}
1790
+ key={session.sessionId}
1791
+ title={sessionTabLabel(session, sessions)}
1792
+ onClick={() => {
1793
+ if (session.sessionId) onFocusSession?.(session.sessionId)
1794
+ }}
1795
+ >
1796
+ {sessionTabLabel(session, sessions)}
1797
+ </button>
1798
+ )
1799
+ })}
1800
+ </div>
1801
+ )}
1802
+ {sessions.length > 0 && (
1366
1803
  <SessionPanel
1367
- key={session.sessionId}
1368
- intent={session}
1369
- focused={session.sessionId === (focusedSessionId ?? intent.sessionId)}
1804
+ intent={intent}
1805
+ focused
1370
1806
  naming={naming}
1807
+ attachedSession={attachedSession}
1371
1808
  onFocus={() => {
1372
- if (session.sessionId) onFocusSession?.(session.sessionId)
1809
+ if (intent.sessionId) onFocusSession?.(intent.sessionId)
1373
1810
  }}
1374
1811
  onWorkflowAction={onWorkflowAction}
1375
1812
  onNavigateDiff={onNavigateDiff}
1376
1813
  />
1377
- ))}
1814
+ )}
1815
+ {showBranchChanges && branchChanges && (
1816
+ <BranchChangesPanel changes={branchChanges} />
1817
+ )}
1378
1818
  </div>
1379
1819
  )}
1380
1820
 
@@ -1416,7 +1856,7 @@ export function HUD({
1416
1856
  selectedAddedImports.length > 0) && (
1417
1857
  <>
1418
1858
  <div className="hud-section-title hud-section-title-edit">
1419
- LLM changes
1859
+ {showBranchChanges ? 'Branch changes' : 'LLM changes'}
1420
1860
  </div>
1421
1861
  <PatchSymbolChanges
1422
1862
  title="Functions"
@@ -1675,7 +2115,7 @@ export function HUD({
1675
2115
  ))}
1676
2116
  </ul>
1677
2117
  )}
1678
- {creatingBlueprint && mapping && onMapAddFile && onMapAddFolder && (
2118
+ {canPlace && mapping && onMapAddFile && onMapAddFolder && (
1679
2119
  <div className="hud-decide hud-map-blueprint">
1680
2120
  <button
1681
2121
  className="hud-button hud-button-approve"
@@ -1795,6 +2235,31 @@ export function HUD({
1795
2235
  >
1796
2236
  {updatingModel ? 'Updating…' : 'Update model'}
1797
2237
  </button>
2238
+ {onSetupSession && (
2239
+ <button
2240
+ className="hud-button"
2241
+ type="button"
2242
+ aria-label="Start an LLM session without attaching a chat yet"
2243
+ title={setupError ?? 'Start an LLM session without attaching a chat yet'}
2244
+ disabled={setupBusy}
2245
+ onClick={() => {
2246
+ setInstructionsOpen(false)
2247
+ setSetupError(null)
2248
+ setSetupBusy(true)
2249
+ void onSetupSession()
2250
+ .catch((caught) => {
2251
+ setSetupError(
2252
+ caught instanceof Error
2253
+ ? caught.message
2254
+ : 'Could not set up the session',
2255
+ )
2256
+ })
2257
+ .finally(() => setSetupBusy(false))
2258
+ }}
2259
+ >
2260
+ {setupBusy ? 'Starting…' : 'Setup LLM session'}
2261
+ </button>
2262
+ )}
1798
2263
  </div>
1799
2264
  <div className="hud-icon-row">
1800
2265
  {mapping && hasChangeSet && onToggleChangePathsOnly && (
@@ -1908,6 +2373,53 @@ export function HUD({
1908
2373
  {importedBy ? 'K show imports' : 'K show imported by'}
1909
2374
  </span>
1910
2375
  </button>
2376
+ <button
2377
+ className="hud-button hud-icon-button"
2378
+ data-active={showBranchChanges}
2379
+ aria-label={
2380
+ llmMakingChanges
2381
+ ? 'Show branch changes unavailable while the LLM is making changes'
2382
+ : showBranchChanges
2383
+ ? 'Hide branch changes'
2384
+ : 'Show branch changes'
2385
+ }
2386
+ aria-keyshortcuts="G"
2387
+ aria-pressed={showBranchChanges}
2388
+ aria-disabled={!canShowBranchChanges}
2389
+ type="button"
2390
+ onClick={() => {
2391
+ if (!canShowBranchChanges) return
2392
+ onToggleShowBranchChanges?.()
2393
+ }}
2394
+ >
2395
+ <svg
2396
+ viewBox="0 0 24 24"
2397
+ width="18"
2398
+ height="18"
2399
+ fill="none"
2400
+ stroke="currentColor"
2401
+ strokeWidth="2"
2402
+ strokeLinecap="round"
2403
+ strokeLinejoin="round"
2404
+ aria-hidden="true"
2405
+ >
2406
+ <circle cx="6" cy="5" r="2.4" />
2407
+ <circle cx="6" cy="19" r="2.4" />
2408
+ <circle cx="18" cy="12" r="2.4" />
2409
+ <path d="M6 7.4v9.2" />
2410
+ <path d="M6 12h7.2" />
2411
+ <path d="M13.2 12c2.2 0 2.2-4.6 4.4-4.6" />
2412
+ </svg>
2413
+ <span className="hud-tooltip">
2414
+ {llmMakingChanges
2415
+ ? 'Unavailable while the LLM is making changes'
2416
+ : !canShowBranchChanges
2417
+ ? 'No git branch to show'
2418
+ : showBranchChanges
2419
+ ? 'G hide branch changes'
2420
+ : 'G show branch changes'}
2421
+ </span>
2422
+ </button>
1911
2423
  <button
1912
2424
  className="hud-button hud-icon-button"
1913
2425
  data-active={followLook}