@jkwd/inbase 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
+ On the map, right-click to add a file or folder. 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: false }
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,32 @@ 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
+ {!intent.awaitingAttach && (
610
+ <LiveStatus
611
+ intent={intent}
612
+ showStop={showConnectedProgress || working}
613
+ onStop={() => act('stop')}
614
+ />
615
+ )}
358
616
  <label className="hud-mode-switch">
359
617
  <span>Step by step</span>
360
618
  <button
@@ -373,13 +631,35 @@ function SessionPanel({
373
631
  Accept proposal.
374
632
  </p>
375
633
  )}
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 ? (
634
+ {handshakeSetup ? (
635
+ <HandshakeSetup
636
+ instruction={initialInstruction}
637
+ onInstructionChange={updateInitialInstruction}
638
+ blueprintDefined={blueprintIsDefined(intent)}
639
+ awaitingAttach={Boolean(intent.awaitingAttach)}
640
+ attachBlockedBy={attachBlockedBy || null}
641
+ />
642
+ ) : intent.awaitingAttach ? (
643
+ attachedSession &&
644
+ attachedSession.sessionId !== sessionId ? (
645
+ <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>.
649
+ </p>
650
+ ) : (
651
+ <p className="hud-mode-hint">
652
+ No LLM is attached. Open a Cursor chat and run{' '}
653
+ <kbd>/inbase</kbd>. It connects to this focused session.
654
+ </p>
655
+ )
656
+ ) : null}
657
+ {intent.feature &&
658
+ !handshakeSetup &&
659
+ intent.feature.trim() !== sessionLabel(intent) && (
660
+ <p className="hud-feature">{intent.feature}</p>
661
+ )}
662
+ {askingBlueprint && !handshakeSetup && !showConnectedProgress ? (
383
663
  <>
384
664
  <p>
385
665
  Place files and folders for this chat, then send them as a
@@ -410,64 +690,9 @@ function SessionPanel({
410
690
  </button>
411
691
  </div>
412
692
  </>
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' ? (
693
+ ) : handshakeSetup ? null : intent.status === 'finished' ? (
439
694
  <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
- ) : (
695
+ ) : showConnectedProgress || preparing ? null : (
471
696
  <p>
472
697
  {stepLabel}
473
698
  {intent.reason ? ` · ${intent.reason}` : ''}
@@ -475,10 +700,11 @@ function SessionPanel({
475
700
  )}
476
701
  {!askingBlueprint && !sendingBlueprint && (
477
702
  <>
478
- {canPlace && (
703
+ {canPlace && !intent.working && (
479
704
  <p>
480
- <kbd>Space</kbd> places a file, <kbd>B</kbd> an island for
481
- this session.
705
+ On the map, right-click to add a file or folder.{' '}
706
+ <kbd>Space</kbd> places a file, <kbd>B</kbd> an island while
707
+ walking.
482
708
  </p>
483
709
  )}
484
710
  {intent.steps?.length > 0 && (
@@ -487,6 +713,11 @@ function SessionPanel({
487
713
  const proposed = proposalStep === step.index
488
714
  const processing = processingStep === step.index
489
715
  const accepted = acceptedSteps.has(step.index)
716
+ const creating = processing && !proposed
717
+ const showStepAction =
718
+ creating ||
719
+ (canRunNext && invokeStep?.index === step.index) ||
720
+ (canAcceptProposal && proposed)
490
721
  return (
491
722
  <li
492
723
  key={step.index}
@@ -496,11 +727,12 @@ function SessionPanel({
496
727
  <span className="hud-step-index">{step.index}.</span>
497
728
  <span className="hud-step-main">
498
729
  <span className="hud-step-title">{step.title}</span>
499
- {((canRunNext && invokeStep?.index === step.index) ||
500
- (canAcceptProposal && proposed)) && (
730
+ {showStepAction && (
501
731
  <button
502
732
  className="hud-button hud-button-approve hud-run-step"
503
733
  type="button"
734
+ disabled={creating}
735
+ aria-busy={creating}
504
736
  onClick={() =>
505
737
  proposed
506
738
  ? lastStep
@@ -513,7 +745,11 @@ function SessionPanel({
513
745
  })
514
746
  }
515
747
  >
516
- {proposed ? 'Accept proposal' : 'Run step'}
748
+ {proposed
749
+ ? 'Accept proposal'
750
+ : creating
751
+ ? 'Creating proposal…'
752
+ : 'Create proposal'}
517
753
  </button>
518
754
  )}
519
755
  </span>
@@ -677,6 +913,7 @@ function SessionPanel({
677
913
  rows={3}
678
914
  placeholder="Describe what should change in the next diff…"
679
915
  onChange={(event) => setInstruction(event.target.value)}
916
+ onKeyDown={(event) => event.stopPropagation()}
680
917
  />
681
918
  </label>
682
919
  <div className="hud-decide">
@@ -708,6 +945,131 @@ function SessionPanel({
708
945
  )
709
946
  }
710
947
 
948
+ function BranchChangesPanel({
949
+ changes,
950
+ }: {
951
+ changes: BranchChanges
952
+ }) {
953
+ const [minimized, setMinimized] = useState(false)
954
+ const addedFunctions = changes.addedFunctions ?? []
955
+ const addedVariables = changes.addedVariables ?? []
956
+ const addedImports = changes.addedImports ?? []
957
+ const changedFunctions = changes.changedFunctions ?? []
958
+ const changedVariables = changes.changedVariables ?? []
959
+ const subtitle =
960
+ changes.branch && changes.base
961
+ ? `${changes.branch} vs ${changes.base}`
962
+ : changes.branch
963
+ ? changes.branch
964
+ : null
965
+ const hasContent =
966
+ changes.files.length > 0 ||
967
+ (changes.createFolders ?? []).length > 0 ||
968
+ changes.creates.length > 0 ||
969
+ changes.deletes.length > 0 ||
970
+ addedFunctions.length > 0 ||
971
+ addedVariables.length > 0 ||
972
+ addedImports.length > 0 ||
973
+ changedFunctions.length > 0 ||
974
+ changedVariables.length > 0 ||
975
+ (changes.imports ?? []).length > 0
976
+
977
+ return (
978
+ <aside
979
+ className="hud-panel hud-panel-planned hud-panel-done"
980
+ data-minimized={minimized}
981
+ >
982
+ <PanelChrome
983
+ title="Branch changes"
984
+ minimized={minimized}
985
+ onMinimize={() => setMinimized((current) => !current)}
986
+ />
987
+ {!minimized && (
988
+ <>
989
+ {subtitle && <p className="hud-feature">{subtitle}</p>}
990
+ {!hasContent ? (
991
+ <p>No file changes on this branch.</p>
992
+ ) : (
993
+ <MutationFold hasContent>
994
+ {changes.files.length > 0 && (
995
+ <>
996
+ <div className="hud-section-title hud-section-title-edit">
997
+ Changed
998
+ </div>
999
+ <ul>
1000
+ {changes.files.map((id) => (
1001
+ <li className="hud-file-edit" key={id}>
1002
+ {id}
1003
+ </li>
1004
+ ))}
1005
+ </ul>
1006
+ </>
1007
+ )}
1008
+ {(changes.createFolders ?? []).length > 0 && (
1009
+ <>
1010
+ <div className="hud-section-title hud-section-title-add">
1011
+ Added islands
1012
+ </div>
1013
+ <ul>
1014
+ {changes.createFolders.map((id) => (
1015
+ <li className="hud-file-add" key={id}>
1016
+ {id}/
1017
+ </li>
1018
+ ))}
1019
+ </ul>
1020
+ </>
1021
+ )}
1022
+ {changes.creates.length > 0 && (
1023
+ <>
1024
+ <div className="hud-section-title hud-section-title-add">
1025
+ Added
1026
+ </div>
1027
+ <ul>
1028
+ {changes.creates.map((id) => (
1029
+ <li className="hud-file-add" key={id}>
1030
+ {id}
1031
+ </li>
1032
+ ))}
1033
+ </ul>
1034
+ </>
1035
+ )}
1036
+ {changes.deletes.length > 0 && (
1037
+ <>
1038
+ <div className="hud-section-title hud-section-title-remove">
1039
+ Removed
1040
+ </div>
1041
+ <ul>
1042
+ {changes.deletes.map((id) => (
1043
+ <li className="hud-file-remove" key={id}>
1044
+ {id}
1045
+ </li>
1046
+ ))}
1047
+ </ul>
1048
+ </>
1049
+ )}
1050
+ <PatchSymbolChanges
1051
+ title="Functions"
1052
+ added={addedFunctions}
1053
+ changed={changedFunctions}
1054
+ />
1055
+ <PatchSymbolChanges
1056
+ title="Vars"
1057
+ added={addedVariables}
1058
+ changed={changedVariables}
1059
+ />
1060
+ <PanelList
1061
+ title="Imports"
1062
+ items={importLabels(addedImports)}
1063
+ tone="add"
1064
+ />
1065
+ </MutationFold>
1066
+ )}
1067
+ </>
1068
+ )}
1069
+ </aside>
1070
+ )
1071
+ }
1072
+
711
1073
  type ExplorerInstruction = {
712
1074
  id: string
713
1075
  keys: string[]
@@ -751,6 +1113,8 @@ function explorerInstructions({
751
1113
  importedBy,
752
1114
  canStop,
753
1115
  sessionCount,
1116
+ showBranchChanges,
1117
+ canShowBranchChanges,
754
1118
  }: {
755
1119
  canPlace: boolean
756
1120
  hasChangeSet: boolean
@@ -761,6 +1125,8 @@ function explorerInstructions({
761
1125
  importedBy: boolean
762
1126
  canStop: boolean
763
1127
  sessionCount: number
1128
+ showBranchChanges: boolean
1129
+ canShowBranchChanges: boolean
764
1130
  }): ExplorerInstructionSection[] {
765
1131
  const backspace: ExplorerInstruction[] =
766
1132
  selectedUserCreated && canPlace
@@ -781,6 +1147,17 @@ function explorerInstructions({
781
1147
  keys: ['K'],
782
1148
  label: importedBy ? 'Show imports' : 'Show imported by',
783
1149
  }
1150
+ const branch: ExplorerInstruction[] = canShowBranchChanges
1151
+ ? [
1152
+ {
1153
+ id: 'branch-changes',
1154
+ keys: ['G'],
1155
+ label: showBranchChanges
1156
+ ? 'Hide branch changes'
1157
+ : 'Show branch changes',
1158
+ },
1159
+ ]
1160
+ : []
784
1161
  const stop: ExplorerInstruction[] = canStop
785
1162
  ? [
786
1163
  {
@@ -821,11 +1198,17 @@ function explorerInstructions({
821
1198
  { id: 'aim-line', keys: ['Click'], label: 'Aim a line to fly' },
822
1199
  ...info,
823
1200
  imported,
1201
+ ...branch,
824
1202
  {
825
1203
  id: 'update-model',
826
1204
  keys: ['Update model'],
827
1205
  label: 'Rescan and rebuild the map',
828
1206
  },
1207
+ {
1208
+ id: 'setup-session',
1209
+ keys: ['Setup LLM session'],
1210
+ label: 'Open a session; /inbase connects the focused one',
1211
+ },
829
1212
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
830
1213
  {
831
1214
  id: 'release',
@@ -871,8 +1254,8 @@ function explorerInstructions({
871
1254
  { id: 'select-island', keys: ['Click'], label: 'Select an island' },
872
1255
  {
873
1256
  id: 'add-file-folder',
874
- keys: [],
875
- label: 'Add file / Add folder in panel',
1257
+ keys: ['Right-click'],
1258
+ label: 'Add file or folder',
876
1259
  },
877
1260
  ]
878
1261
  : []),
@@ -902,11 +1285,17 @@ function explorerInstructions({
902
1285
  ...info,
903
1286
  thumbnail,
904
1287
  imported,
1288
+ ...branch,
905
1289
  {
906
1290
  id: 'update-model',
907
1291
  keys: ['Update model'],
908
1292
  label: 'Rescan and rebuild the map',
909
1293
  },
1294
+ {
1295
+ id: 'setup-session',
1296
+ keys: ['Setup LLM session'],
1297
+ label: 'Open a session; /inbase connects the focused one',
1298
+ },
910
1299
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
911
1300
  ...stop,
912
1301
  ],
@@ -930,6 +1319,7 @@ type HUDProps = {
930
1319
  intents?: AgentIntent[]
931
1320
  focusedSessionId?: string | null
932
1321
  onFocusSession?: (sessionId: string) => void
1322
+ onSetupSession?: () => Promise<unknown>
933
1323
  onWorkflowAction: (
934
1324
  sessionId: string,
935
1325
  action: WorkflowAction,
@@ -940,6 +1330,11 @@ type HUDProps = {
940
1330
  onWalk: () => void
941
1331
  followLook: boolean
942
1332
  onToggleFollowLook: () => void
1333
+ showBranchChanges?: boolean
1334
+ branchChanges?: BranchChanges
1335
+ canShowBranchChanges?: boolean
1336
+ llmMakingChanges?: boolean
1337
+ onToggleShowBranchChanges?: () => void
943
1338
  onUpdateModel: () => void
944
1339
  updatingModel?: boolean
945
1340
  importedBy: boolean
@@ -989,12 +1384,18 @@ export function HUD({
989
1384
  intents,
990
1385
  focusedSessionId = null,
991
1386
  onFocusSession,
1387
+ onSetupSession,
992
1388
  onWorkflowAction,
993
1389
  onNavigateDiff,
994
1390
  onOpenMap,
995
1391
  onWalk,
996
1392
  followLook,
997
1393
  onToggleFollowLook,
1394
+ showBranchChanges = false,
1395
+ branchChanges,
1396
+ canShowBranchChanges = false,
1397
+ llmMakingChanges = false,
1398
+ onToggleShowBranchChanges,
998
1399
  onUpdateModel,
999
1400
  updatingModel = false,
1000
1401
  importedBy,
@@ -1043,9 +1444,13 @@ export function HUD({
1043
1444
  (item) => item.sessionId && isReviewingIntent(item.status),
1044
1445
  )
1045
1446
  const canStop = canStopSession(intent)
1447
+ const attachedSession =
1448
+ sessions.find((session) => session.awaitingAttach === false) ?? null
1046
1449
  const [walkIntro, setWalkIntro] = useState(false)
1047
1450
  const walkIntroSeen = useRef(false)
1048
1451
  const [instructionsOpen, setInstructionsOpen] = useState(false)
1452
+ const [setupError, setSetupError] = useState<string | null>(null)
1453
+ const [setupBusy, setSetupBusy] = useState(false)
1049
1454
  const [infoVisible, setInfoVisible] = useState(false)
1050
1455
  const [infoMinimized, setInfoMinimized] = useState(false)
1051
1456
  const [thumbnailVisible, setThumbnailVisible] = useState(true)
@@ -1053,12 +1458,13 @@ export function HUD({
1053
1458
  const [thumbnailMaximized, setThumbnailMaximized] = useState(false)
1054
1459
  const infoPanelRef = useRef<HTMLDivElement>(null)
1055
1460
  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 ?? []
1461
+ const overlay = showBranchChanges && branchChanges ? branchChanges : intent
1462
+ const previewing = intent.preview || showBranchChanges
1463
+ const addedFunctions = overlay.addedFunctions ?? []
1464
+ const addedVariables = overlay.addedVariables ?? []
1465
+ const addedImports = overlay.addedImports ?? []
1466
+ const changedFunctions = overlay.changedFunctions ?? []
1467
+ const changedVariables = overlay.changedVariables ?? []
1062
1468
  const selectedAddedFunctions = selected
1063
1469
  ? addedFunctions.filter((item) => item.file === selected.id)
1064
1470
  : []
@@ -1285,6 +1691,8 @@ export function HUD({
1285
1691
  importedBy,
1286
1692
  canStop,
1287
1693
  sessionCount: sessions.length,
1694
+ showBranchChanges,
1695
+ canShowBranchChanges,
1288
1696
  })
1289
1697
  const currentInstructionView: InstructionView = mapping
1290
1698
  ? thumbnailMaximized
@@ -1368,7 +1776,7 @@ export function HUD({
1368
1776
  {selected && <div className="hud-chip">{selected.path}</div>}
1369
1777
  </div>
1370
1778
 
1371
- {sessions.length > 0 && (
1779
+ {(sessions.length > 0 || showBranchChanges) && (
1372
1780
  <div className="hud-left-stack">
1373
1781
  {sessions.length > 1 && (
1374
1782
  <div className="hud-session-tabs" role="tablist" aria-label="LLM sessions">
@@ -1383,6 +1791,7 @@ export function HUD({
1383
1791
  aria-selected={active}
1384
1792
  data-active={active}
1385
1793
  key={session.sessionId}
1794
+ title={sessionTabLabel(session, sessions)}
1386
1795
  onClick={() => {
1387
1796
  if (session.sessionId) onFocusSession?.(session.sessionId)
1388
1797
  }}
@@ -1393,16 +1802,22 @@ export function HUD({
1393
1802
  })}
1394
1803
  </div>
1395
1804
  )}
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
- />
1805
+ {sessions.length > 0 && (
1806
+ <SessionPanel
1807
+ intent={intent}
1808
+ focused
1809
+ naming={naming}
1810
+ attachedSession={attachedSession}
1811
+ onFocus={() => {
1812
+ if (intent.sessionId) onFocusSession?.(intent.sessionId)
1813
+ }}
1814
+ onWorkflowAction={onWorkflowAction}
1815
+ onNavigateDiff={onNavigateDiff}
1816
+ />
1817
+ )}
1818
+ {showBranchChanges && branchChanges && (
1819
+ <BranchChangesPanel changes={branchChanges} />
1820
+ )}
1406
1821
  </div>
1407
1822
  )}
1408
1823
 
@@ -1444,7 +1859,7 @@ export function HUD({
1444
1859
  selectedAddedImports.length > 0) && (
1445
1860
  <>
1446
1861
  <div className="hud-section-title hud-section-title-edit">
1447
- LLM changes
1862
+ {showBranchChanges ? 'Branch changes' : 'LLM changes'}
1448
1863
  </div>
1449
1864
  <PatchSymbolChanges
1450
1865
  title="Functions"
@@ -1823,6 +2238,31 @@ export function HUD({
1823
2238
  >
1824
2239
  {updatingModel ? 'Updating…' : 'Update model'}
1825
2240
  </button>
2241
+ {onSetupSession && (
2242
+ <button
2243
+ className="hud-button"
2244
+ type="button"
2245
+ aria-label="Start an LLM session without attaching a chat yet"
2246
+ title={setupError ?? 'Start an LLM session without attaching a chat yet'}
2247
+ disabled={setupBusy}
2248
+ onClick={() => {
2249
+ setInstructionsOpen(false)
2250
+ setSetupError(null)
2251
+ setSetupBusy(true)
2252
+ void onSetupSession()
2253
+ .catch((caught) => {
2254
+ setSetupError(
2255
+ caught instanceof Error
2256
+ ? caught.message
2257
+ : 'Could not set up the session',
2258
+ )
2259
+ })
2260
+ .finally(() => setSetupBusy(false))
2261
+ }}
2262
+ >
2263
+ {setupBusy ? 'Starting…' : 'Setup LLM session'}
2264
+ </button>
2265
+ )}
1826
2266
  </div>
1827
2267
  <div className="hud-icon-row">
1828
2268
  {mapping && hasChangeSet && onToggleChangePathsOnly && (
@@ -1936,6 +2376,53 @@ export function HUD({
1936
2376
  {importedBy ? 'K show imports' : 'K show imported by'}
1937
2377
  </span>
1938
2378
  </button>
2379
+ <button
2380
+ className="hud-button hud-icon-button"
2381
+ data-active={showBranchChanges}
2382
+ aria-label={
2383
+ llmMakingChanges
2384
+ ? 'Show branch changes unavailable while the LLM is making changes'
2385
+ : showBranchChanges
2386
+ ? 'Hide branch changes'
2387
+ : 'Show branch changes'
2388
+ }
2389
+ aria-keyshortcuts="G"
2390
+ aria-pressed={showBranchChanges}
2391
+ aria-disabled={!canShowBranchChanges}
2392
+ type="button"
2393
+ onClick={() => {
2394
+ if (!canShowBranchChanges) return
2395
+ onToggleShowBranchChanges?.()
2396
+ }}
2397
+ >
2398
+ <svg
2399
+ viewBox="0 0 24 24"
2400
+ width="18"
2401
+ height="18"
2402
+ fill="none"
2403
+ stroke="currentColor"
2404
+ strokeWidth="2"
2405
+ strokeLinecap="round"
2406
+ strokeLinejoin="round"
2407
+ aria-hidden="true"
2408
+ >
2409
+ <circle cx="6" cy="5" r="2.4" />
2410
+ <circle cx="6" cy="19" r="2.4" />
2411
+ <circle cx="18" cy="12" r="2.4" />
2412
+ <path d="M6 7.4v9.2" />
2413
+ <path d="M6 12h7.2" />
2414
+ <path d="M13.2 12c2.2 0 2.2-4.6 4.4-4.6" />
2415
+ </svg>
2416
+ <span className="hud-tooltip">
2417
+ {llmMakingChanges
2418
+ ? 'Unavailable while the LLM is making changes'
2419
+ : !canShowBranchChanges
2420
+ ? 'No git branch to show'
2421
+ : showBranchChanges
2422
+ ? 'G hide branch changes'
2423
+ : 'G show branch changes'}
2424
+ </span>
2425
+ </button>
1939
2426
  <button
1940
2427
  className="hud-button hud-icon-button"
1941
2428
  data-active={followLook}