@jkwd/inbase 0.1.10 → 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,10 +31,14 @@ 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
+
32
38
  function sessionTabLabel(intent: AgentIntent, sessions: AgentIntent[]) {
33
- const title = intent.feature?.trim() || reviewTitle(intent.status)
39
+ const title = sessionLabel(intent) || reviewTitle(intent.status)
34
40
  const same = sessions.filter(
35
- (item) => (item.feature?.trim() || reviewTitle(item.status)) === title,
41
+ (item) => (sessionLabel(item) || reviewTitle(item.status)) === title,
36
42
  )
37
43
  if (same.length < 2) return title
38
44
  const id = intent.sessionId ?? ''
@@ -210,18 +216,29 @@ function AddIntentRow({
210
216
 
211
217
  function PanelChrome({
212
218
  title,
219
+ subtitle,
213
220
  minimized = false,
214
221
  onMinimize,
215
222
  onClose,
223
+ closeLabel = 'Close',
224
+ closeReject = false,
216
225
  }: {
217
226
  title: ReactNode
227
+ subtitle?: ReactNode
218
228
  minimized?: boolean
219
229
  onMinimize?: () => void
220
230
  onClose?: () => void
231
+ closeLabel?: string
232
+ closeReject?: boolean
221
233
  }) {
222
234
  return (
223
235
  <div className="hud-panel-chrome">
224
- <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>
225
242
  <div className="hud-panel-controls">
226
243
  {onMinimize && (
227
244
  <button
@@ -235,9 +252,13 @@ function PanelChrome({
235
252
  )}
236
253
  {onClose && (
237
254
  <button
238
- 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
+ }
239
260
  type="button"
240
- aria-label="Close"
261
+ aria-label={closeLabel}
241
262
  onClick={onClose}
242
263
  >
243
264
  ×
@@ -248,10 +269,200 @@ function PanelChrome({
248
269
  )
249
270
  }
250
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
+
251
461
  type SessionPanelProps = {
252
462
  intent: AgentIntent
253
463
  focused: boolean
254
464
  naming: boolean
465
+ attachedSession?: AgentIntent | null
255
466
  onFocus: () => void
256
467
  onWorkflowAction: (
257
468
  sessionId: string,
@@ -265,12 +476,16 @@ function SessionPanel({
265
476
  intent,
266
477
  focused,
267
478
  naming,
479
+ attachedSession = null,
268
480
  onFocus,
269
481
  onWorkflowAction,
270
482
  onNavigateDiff,
271
483
  }: SessionPanelProps) {
272
484
  const [minimized, setMinimized] = useState(false)
273
485
  const [instruction, setInstruction] = useState('')
486
+ const [initialInstruction, setInitialInstruction] = useState(
487
+ () => intent.initialInstruction ?? '',
488
+ )
274
489
  const sessionId = intent.sessionId
275
490
  const pending = intent.status === 'pending' && intent.isActiveDiff
276
491
  const askingBlueprint = intent.status === 'blueprint_ask'
@@ -330,8 +545,31 @@ function SessionPanel({
330
545
  setInstruction('')
331
546
  }, [intent.diffId])
332
547
 
548
+ useEffect(() => {
549
+ setInitialInstruction(intent.initialInstruction ?? '')
550
+ }, [sessionId])
551
+
333
552
  if (!sessionId || !isReviewingIntent(intent.status)) return null
334
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
+
335
573
  const act = (
336
574
  action: WorkflowAction,
337
575
  options?: { instruction?: string; step?: number; stepByStep?: boolean },
@@ -349,12 +587,30 @@ function SessionPanel({
349
587
  onPointerDown={onFocus}
350
588
  >
351
589
  <PanelChrome
352
- 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
+ }
353
601
  minimized={minimized}
354
602
  onMinimize={() => setMinimized((current) => !current)}
603
+ onClose={() => act('stop')}
604
+ closeLabel="Stop session"
605
+ closeReject
355
606
  />
356
607
  {!minimized && (
357
608
  <>
609
+ <LiveStatus
610
+ intent={intent}
611
+ showStop={showConnectedProgress || working}
612
+ onStop={() => act('stop')}
613
+ />
358
614
  <label className="hud-mode-switch">
359
615
  <span>Step by step</span>
360
616
  <button
@@ -373,13 +629,35 @@ function SessionPanel({
373
629
  Accept proposal.
374
630
  </p>
375
631
  )}
376
- {intent.llmIdle && !working && !preparing && (
377
- <p className="hud-mode-hint">
378
- LLM is idle. Current changes stay on the map.
379
- </p>
380
- )}
381
- {intent.feature && <p className="hud-feature">{intent.feature}</p>}
382
- {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 ? (
383
661
  <>
384
662
  <p>
385
663
  Place files and folders for this chat, then send them as a
@@ -410,64 +688,9 @@ function SessionPanel({
410
688
  </button>
411
689
  </div>
412
690
  </>
413
- ) : sendingBlueprint ? (
414
- <>
415
- <p>
416
- Walk the map, press <kbd>Space</kbd> for a file and{' '}
417
- <kbd>B</kbd> for an island. Send the blueprint when the layout
418
- is ready. You can keep placing files after this handshake.
419
- </p>
420
- <div className="hud-decide">
421
- <button
422
- className="hud-button hud-button-approve"
423
- type="button"
424
- disabled={naming}
425
- onClick={() => act('blueprint_send')}
426
- >
427
- Send blueprint
428
- </button>
429
- <button
430
- className="hud-button hud-button-reject"
431
- type="button"
432
- onClick={() => act('stop')}
433
- >
434
- Stop
435
- </button>
436
- </div>
437
- </>
438
- ) : intent.status === 'finished' ? (
691
+ ) : handshakeSetup ? null : intent.status === 'finished' ? (
439
692
  <p>All plan steps were applied.</p>
440
- ) : preparing ? (
441
- <div className="hud-working">
442
- <span className="hud-spinner" aria-hidden="true" />
443
- <span>LLM preparing…</span>
444
- <button
445
- className="hud-button hud-button-reject"
446
- type="button"
447
- onClick={() => act('stop')}
448
- >
449
- Stop
450
- </button>
451
- </div>
452
- ) : working ? (
453
- <div className="hud-working">
454
- <span className="hud-spinner" aria-hidden="true" />
455
- <span>
456
- {intent.status === 'replanning'
457
- ? 'Updating the remaining plan from your instruction…'
458
- : intent.stalledWait
459
- ? 'The LLM is still waiting on this step…'
460
- : `Implementing ${stepLabel.toLowerCase()}…`}
461
- </span>
462
- <button
463
- className="hud-button hud-button-reject"
464
- type="button"
465
- onClick={() => act('stop')}
466
- >
467
- Stop
468
- </button>
469
- </div>
470
- ) : (
693
+ ) : showConnectedProgress || preparing ? null : (
471
694
  <p>
472
695
  {stepLabel}
473
696
  {intent.reason ? ` · ${intent.reason}` : ''}
@@ -475,7 +698,7 @@ function SessionPanel({
475
698
  )}
476
699
  {!askingBlueprint && !sendingBlueprint && (
477
700
  <>
478
- {canPlace && (
701
+ {canPlace && !intent.working && (
479
702
  <p>
480
703
  <kbd>Space</kbd> places a file, <kbd>B</kbd> an island for
481
704
  this session.
@@ -487,6 +710,11 @@ function SessionPanel({
487
710
  const proposed = proposalStep === step.index
488
711
  const processing = processingStep === step.index
489
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)
490
718
  return (
491
719
  <li
492
720
  key={step.index}
@@ -496,11 +724,12 @@ function SessionPanel({
496
724
  <span className="hud-step-index">{step.index}.</span>
497
725
  <span className="hud-step-main">
498
726
  <span className="hud-step-title">{step.title}</span>
499
- {((canRunNext && invokeStep?.index === step.index) ||
500
- (canAcceptProposal && proposed)) && (
727
+ {showStepAction && (
501
728
  <button
502
729
  className="hud-button hud-button-approve hud-run-step"
503
730
  type="button"
731
+ disabled={creating}
732
+ aria-busy={creating}
504
733
  onClick={() =>
505
734
  proposed
506
735
  ? lastStep
@@ -513,7 +742,11 @@ function SessionPanel({
513
742
  })
514
743
  }
515
744
  >
516
- {proposed ? 'Accept proposal' : 'Run step'}
745
+ {proposed
746
+ ? 'Accept proposal'
747
+ : creating
748
+ ? 'Creating proposal…'
749
+ : 'Create proposal'}
517
750
  </button>
518
751
  )}
519
752
  </span>
@@ -677,6 +910,7 @@ function SessionPanel({
677
910
  rows={3}
678
911
  placeholder="Describe what should change in the next diff…"
679
912
  onChange={(event) => setInstruction(event.target.value)}
913
+ onKeyDown={(event) => event.stopPropagation()}
680
914
  />
681
915
  </label>
682
916
  <div className="hud-decide">
@@ -708,6 +942,131 @@ function SessionPanel({
708
942
  )
709
943
  }
710
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
+
711
1070
  type ExplorerInstruction = {
712
1071
  id: string
713
1072
  keys: string[]
@@ -751,6 +1110,8 @@ function explorerInstructions({
751
1110
  importedBy,
752
1111
  canStop,
753
1112
  sessionCount,
1113
+ showBranchChanges,
1114
+ canShowBranchChanges,
754
1115
  }: {
755
1116
  canPlace: boolean
756
1117
  hasChangeSet: boolean
@@ -761,6 +1122,8 @@ function explorerInstructions({
761
1122
  importedBy: boolean
762
1123
  canStop: boolean
763
1124
  sessionCount: number
1125
+ showBranchChanges: boolean
1126
+ canShowBranchChanges: boolean
764
1127
  }): ExplorerInstructionSection[] {
765
1128
  const backspace: ExplorerInstruction[] =
766
1129
  selectedUserCreated && canPlace
@@ -781,6 +1144,17 @@ function explorerInstructions({
781
1144
  keys: ['K'],
782
1145
  label: importedBy ? 'Show imports' : 'Show imported by',
783
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
+ : []
784
1158
  const stop: ExplorerInstruction[] = canStop
785
1159
  ? [
786
1160
  {
@@ -821,11 +1195,17 @@ function explorerInstructions({
821
1195
  { id: 'aim-line', keys: ['Click'], label: 'Aim a line to fly' },
822
1196
  ...info,
823
1197
  imported,
1198
+ ...branch,
824
1199
  {
825
1200
  id: 'update-model',
826
1201
  keys: ['Update model'],
827
1202
  label: 'Rescan and rebuild the map',
828
1203
  },
1204
+ {
1205
+ id: 'setup-session',
1206
+ keys: ['Setup LLM session'],
1207
+ label: 'Open a session; /inbase connects the focused one',
1208
+ },
829
1209
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
830
1210
  {
831
1211
  id: 'release',
@@ -902,11 +1282,17 @@ function explorerInstructions({
902
1282
  ...info,
903
1283
  thumbnail,
904
1284
  imported,
1285
+ ...branch,
905
1286
  {
906
1287
  id: 'update-model',
907
1288
  keys: ['Update model'],
908
1289
  label: 'Rescan and rebuild the map',
909
1290
  },
1291
+ {
1292
+ id: 'setup-session',
1293
+ keys: ['Setup LLM session'],
1294
+ label: 'Open a session; /inbase connects the focused one',
1295
+ },
910
1296
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
911
1297
  ...stop,
912
1298
  ],
@@ -930,6 +1316,7 @@ type HUDProps = {
930
1316
  intents?: AgentIntent[]
931
1317
  focusedSessionId?: string | null
932
1318
  onFocusSession?: (sessionId: string) => void
1319
+ onSetupSession?: () => Promise<unknown>
933
1320
  onWorkflowAction: (
934
1321
  sessionId: string,
935
1322
  action: WorkflowAction,
@@ -940,6 +1327,11 @@ type HUDProps = {
940
1327
  onWalk: () => void
941
1328
  followLook: boolean
942
1329
  onToggleFollowLook: () => void
1330
+ showBranchChanges?: boolean
1331
+ branchChanges?: BranchChanges
1332
+ canShowBranchChanges?: boolean
1333
+ llmMakingChanges?: boolean
1334
+ onToggleShowBranchChanges?: () => void
943
1335
  onUpdateModel: () => void
944
1336
  updatingModel?: boolean
945
1337
  importedBy: boolean
@@ -989,12 +1381,18 @@ export function HUD({
989
1381
  intents,
990
1382
  focusedSessionId = null,
991
1383
  onFocusSession,
1384
+ onSetupSession,
992
1385
  onWorkflowAction,
993
1386
  onNavigateDiff,
994
1387
  onOpenMap,
995
1388
  onWalk,
996
1389
  followLook,
997
1390
  onToggleFollowLook,
1391
+ showBranchChanges = false,
1392
+ branchChanges,
1393
+ canShowBranchChanges = false,
1394
+ llmMakingChanges = false,
1395
+ onToggleShowBranchChanges,
998
1396
  onUpdateModel,
999
1397
  updatingModel = false,
1000
1398
  importedBy,
@@ -1043,9 +1441,13 @@ export function HUD({
1043
1441
  (item) => item.sessionId && isReviewingIntent(item.status),
1044
1442
  )
1045
1443
  const canStop = canStopSession(intent)
1444
+ const attachedSession =
1445
+ sessions.find((session) => session.awaitingAttach === false) ?? null
1046
1446
  const [walkIntro, setWalkIntro] = useState(false)
1047
1447
  const walkIntroSeen = useRef(false)
1048
1448
  const [instructionsOpen, setInstructionsOpen] = useState(false)
1449
+ const [setupError, setSetupError] = useState<string | null>(null)
1450
+ const [setupBusy, setSetupBusy] = useState(false)
1049
1451
  const [infoVisible, setInfoVisible] = useState(false)
1050
1452
  const [infoMinimized, setInfoMinimized] = useState(false)
1051
1453
  const [thumbnailVisible, setThumbnailVisible] = useState(true)
@@ -1053,12 +1455,13 @@ export function HUD({
1053
1455
  const [thumbnailMaximized, setThumbnailMaximized] = useState(false)
1054
1456
  const infoPanelRef = useRef<HTMLDivElement>(null)
1055
1457
  const canPlace = Boolean(intent.creationMode)
1056
- const previewing = intent.preview
1057
- const addedFunctions = intent.addedFunctions ?? []
1058
- const addedVariables = intent.addedVariables ?? []
1059
- const addedImports = intent.addedImports ?? []
1060
- const changedFunctions = intent.changedFunctions ?? []
1061
- const changedVariables = intent.changedVariables ?? []
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 ?? []
1062
1465
  const selectedAddedFunctions = selected
1063
1466
  ? addedFunctions.filter((item) => item.file === selected.id)
1064
1467
  : []
@@ -1285,6 +1688,8 @@ export function HUD({
1285
1688
  importedBy,
1286
1689
  canStop,
1287
1690
  sessionCount: sessions.length,
1691
+ showBranchChanges,
1692
+ canShowBranchChanges,
1288
1693
  })
1289
1694
  const currentInstructionView: InstructionView = mapping
1290
1695
  ? thumbnailMaximized
@@ -1368,7 +1773,7 @@ export function HUD({
1368
1773
  {selected && <div className="hud-chip">{selected.path}</div>}
1369
1774
  </div>
1370
1775
 
1371
- {sessions.length > 0 && (
1776
+ {(sessions.length > 0 || showBranchChanges) && (
1372
1777
  <div className="hud-left-stack">
1373
1778
  {sessions.length > 1 && (
1374
1779
  <div className="hud-session-tabs" role="tablist" aria-label="LLM sessions">
@@ -1383,6 +1788,7 @@ export function HUD({
1383
1788
  aria-selected={active}
1384
1789
  data-active={active}
1385
1790
  key={session.sessionId}
1791
+ title={sessionTabLabel(session, sessions)}
1386
1792
  onClick={() => {
1387
1793
  if (session.sessionId) onFocusSession?.(session.sessionId)
1388
1794
  }}
@@ -1393,16 +1799,22 @@ export function HUD({
1393
1799
  })}
1394
1800
  </div>
1395
1801
  )}
1396
- <SessionPanel
1397
- intent={intent}
1398
- focused
1399
- naming={naming}
1400
- onFocus={() => {
1401
- if (intent.sessionId) onFocusSession?.(intent.sessionId)
1402
- }}
1403
- onWorkflowAction={onWorkflowAction}
1404
- onNavigateDiff={onNavigateDiff}
1405
- />
1802
+ {sessions.length > 0 && (
1803
+ <SessionPanel
1804
+ intent={intent}
1805
+ focused
1806
+ naming={naming}
1807
+ attachedSession={attachedSession}
1808
+ onFocus={() => {
1809
+ if (intent.sessionId) onFocusSession?.(intent.sessionId)
1810
+ }}
1811
+ onWorkflowAction={onWorkflowAction}
1812
+ onNavigateDiff={onNavigateDiff}
1813
+ />
1814
+ )}
1815
+ {showBranchChanges && branchChanges && (
1816
+ <BranchChangesPanel changes={branchChanges} />
1817
+ )}
1406
1818
  </div>
1407
1819
  )}
1408
1820
 
@@ -1444,7 +1856,7 @@ export function HUD({
1444
1856
  selectedAddedImports.length > 0) && (
1445
1857
  <>
1446
1858
  <div className="hud-section-title hud-section-title-edit">
1447
- LLM changes
1859
+ {showBranchChanges ? 'Branch changes' : 'LLM changes'}
1448
1860
  </div>
1449
1861
  <PatchSymbolChanges
1450
1862
  title="Functions"
@@ -1823,6 +2235,31 @@ export function HUD({
1823
2235
  >
1824
2236
  {updatingModel ? 'Updating…' : 'Update model'}
1825
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
+ )}
1826
2263
  </div>
1827
2264
  <div className="hud-icon-row">
1828
2265
  {mapping && hasChangeSet && onToggleChangePathsOnly && (
@@ -1936,6 +2373,53 @@ export function HUD({
1936
2373
  {importedBy ? 'K show imports' : 'K show imported by'}
1937
2374
  </span>
1938
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>
1939
2423
  <button
1940
2424
  className="hud-button hud-icon-button"
1941
2425
  data-active={followLook}