@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.
@@ -45,10 +45,26 @@ function normalizeImportAdditions(value: unknown): PatchImportAddition[] {
45
45
  })
46
46
  }
47
47
 
48
+ function normalizeAck(
49
+ value: unknown,
50
+ ): AgentIntent['lastAck'] {
51
+ if (!value || typeof value !== 'object') return null
52
+ const kind = (value as { kind?: unknown }).kind
53
+ if (typeof kind !== 'string' || kind.trim() === '') return null
54
+ const detail = (value as { detail?: unknown }).detail
55
+ const at = (value as { at?: unknown }).at
56
+ return {
57
+ kind,
58
+ detail: typeof detail === 'string' ? detail : '',
59
+ at: typeof at === 'string' ? at : null,
60
+ }
61
+ }
62
+
48
63
  export const emptyIntent: AgentIntent = {
49
64
  updatedAt: null,
50
65
  showMap: false,
51
66
  status: 'idle',
67
+ name: null,
52
68
  feature: null,
53
69
  steps: [],
54
70
  step: null,
@@ -75,6 +91,11 @@ export const emptyIntent: AgentIntent = {
75
91
  phase: null,
76
92
  working: false,
77
93
  stalledWait: false,
94
+ llmIdle: false,
95
+ awaitingAttach: false,
96
+ listening: false,
97
+ lastAck: null,
98
+ initialInstruction: null,
78
99
  creationMode: false,
79
100
  canEnterBlueprint: false,
80
101
  blueprintSessionId: null,
@@ -90,6 +111,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
90
111
  updatedAt: data?.updatedAt ?? null,
91
112
  showMap: Boolean(data?.showMap),
92
113
  status: data?.status ?? 'idle',
114
+ name: data?.name ?? null,
93
115
  feature: data?.feature ?? null,
94
116
  steps: Array.isArray(data?.steps) ? data.steps : [],
95
117
  step: typeof data?.step === 'number' ? data.step : null,
@@ -119,6 +141,12 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
119
141
  phase: data?.phase ?? null,
120
142
  working: Boolean(data?.working),
121
143
  stalledWait: Boolean(data?.stalledWait),
144
+ llmIdle: Boolean(data?.llmIdle),
145
+ awaitingAttach: Boolean(data?.awaitingAttach),
146
+ listening: Boolean(data?.listening),
147
+ lastAck: normalizeAck(data?.lastAck),
148
+ initialInstruction:
149
+ typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
122
150
  creationMode: Boolean(data?.creationMode),
123
151
  canEnterBlueprint: Boolean(data?.canEnterBlueprint),
124
152
  blueprintSessionId:
@@ -222,6 +250,56 @@ export function persistSessionBlueprint(
222
250
  })
223
251
  }
224
252
 
253
+ export function persistInitialInstruction(sessionId: string, instruction: string) {
254
+ fetch('/api/agent-intent', {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({
258
+ action: 'set_initial_instruction',
259
+ sessionId,
260
+ instruction,
261
+ }),
262
+ }).catch(() => {
263
+ // Keep the local instruction if the session handshake is no longer open.
264
+ })
265
+ }
266
+
267
+ export function persistSessionFocus(sessionId: string) {
268
+ fetch('/api/agent-intent', {
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({
272
+ action: 'focus',
273
+ sessionId,
274
+ }),
275
+ }).catch(() => {
276
+ // Keep the local focused session if the server could not record it.
277
+ })
278
+ }
279
+
280
+ export async function setupVisualizerSession(name?: string) {
281
+ const response = await fetch('/api/agent-intent', {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({
285
+ action: 'setup_session',
286
+ name,
287
+ }),
288
+ })
289
+ if (!response.ok) {
290
+ const detail = await response.text()
291
+ let message = detail || 'Could not set up the LLM session'
292
+ try {
293
+ const parsed = JSON.parse(detail) as { error?: string }
294
+ if (parsed?.error) message = parsed.error
295
+ } catch {
296
+ // Use the raw body when it is not JSON.
297
+ }
298
+ throw new Error(message)
299
+ }
300
+ return normalize((await response.json()) as AgentIntent)
301
+ }
302
+
225
303
  export async function inspectTargetFile(payload: {
226
304
  sessionId?: string | null
227
305
  diffId?: string | null
@@ -0,0 +1,54 @@
1
+ import type { BranchChanges } from './types'
2
+
3
+ export const emptyBranchChanges: BranchChanges = {
4
+ available: false,
5
+ branch: null,
6
+ base: null,
7
+ files: [],
8
+ creates: [],
9
+ deletes: [],
10
+ createFolders: [],
11
+ createLines: {},
12
+ imports: [],
13
+ addedFunctions: [],
14
+ addedVariables: [],
15
+ addedImports: [],
16
+ changedFunctions: [],
17
+ changedVariables: [],
18
+ }
19
+
20
+ function normalize(data: Partial<BranchChanges> | null | undefined): BranchChanges {
21
+ return {
22
+ available: Boolean(data?.available),
23
+ branch: typeof data?.branch === 'string' ? data.branch : null,
24
+ base: typeof data?.base === 'string' ? data.base : null,
25
+ files: Array.isArray(data?.files) ? data.files : [],
26
+ creates: Array.isArray(data?.creates) ? data.creates : [],
27
+ deletes: Array.isArray(data?.deletes) ? data.deletes : [],
28
+ createFolders: Array.isArray(data?.createFolders) ? data.createFolders : [],
29
+ createLines:
30
+ data?.createLines && typeof data.createLines === 'object'
31
+ ? data.createLines
32
+ : {},
33
+ imports: Array.isArray(data?.imports) ? data.imports : [],
34
+ addedFunctions: Array.isArray(data?.addedFunctions) ? data.addedFunctions : [],
35
+ addedVariables: Array.isArray(data?.addedVariables) ? data.addedVariables : [],
36
+ addedImports: Array.isArray(data?.addedImports) ? data.addedImports : [],
37
+ changedFunctions: Array.isArray(data?.changedFunctions)
38
+ ? data.changedFunctions
39
+ : [],
40
+ changedVariables: Array.isArray(data?.changedVariables)
41
+ ? data.changedVariables
42
+ : [],
43
+ }
44
+ }
45
+
46
+ export async function fetchBranchChanges(): Promise<BranchChanges> {
47
+ try {
48
+ const response = await fetch(`/api/branch-changes?t=${Date.now()}`)
49
+ if (!response.ok) return emptyBranchChanges
50
+ return normalize((await response.json()) as BranchChanges)
51
+ } catch {
52
+ return emptyBranchChanges
53
+ }
54
+ }
@@ -251,6 +251,19 @@ button {
251
251
  background: rgba(25, 28, 34, 0.94);
252
252
  }
253
253
 
254
+ .map-change-mark {
255
+ pointer-events: none;
256
+ user-select: none;
257
+ color: #f4f7fb;
258
+ font-size: 32px;
259
+ font-weight: 800;
260
+ line-height: 1;
261
+ letter-spacing: 0;
262
+ -webkit-text-stroke: 0.09em #11151c;
263
+ paint-order: stroke fill;
264
+ text-shadow: 0 0 0.12em #11151c;
265
+ }
266
+
254
267
  .map-you-are-here {
255
268
  display: grid;
256
269
  justify-items: center;
@@ -443,6 +456,11 @@ button {
443
456
  flex: none;
444
457
  }
445
458
 
459
+ .hud-icon-button[aria-disabled='true'] {
460
+ opacity: 0.55;
461
+ cursor: default;
462
+ }
463
+
446
464
  .hud-tooltip {
447
465
  position: absolute;
448
466
  right: 0;
@@ -533,6 +551,24 @@ button {
533
551
  overflow-y: auto;
534
552
  }
535
553
 
554
+ .hud-session-tabs {
555
+ display: flex;
556
+ gap: 6px;
557
+ flex: 0 0 auto;
558
+ pointer-events: auto;
559
+ overflow-x: auto;
560
+ max-width: 100%;
561
+ }
562
+
563
+ .hud-session-tab {
564
+ flex: 1 1 0;
565
+ min-width: 0;
566
+ overflow: hidden;
567
+ text-overflow: ellipsis;
568
+ white-space: nowrap;
569
+ font-size: 13px;
570
+ }
571
+
536
572
  .hud-left-stack .hud-panel-planned {
537
573
  position: relative;
538
574
  top: auto;
@@ -553,10 +589,6 @@ button {
553
589
  flex: 0 0 auto;
554
590
  }
555
591
 
556
- .hud-left-stack .hud-panel-planned[data-focused='false'] {
557
- opacity: 0.86;
558
- }
559
-
560
592
  .hud-left-stack .hud-panel-planned[data-focused='true'] {
561
593
  box-shadow: 0 0 0 1px #9ad8ff;
562
594
  }
@@ -628,6 +660,11 @@ button {
628
660
  margin: -4px 0 8px;
629
661
  }
630
662
 
663
+ .hud-panel-chrome-heading {
664
+ min-width: 0;
665
+ flex: 1 1 auto;
666
+ }
667
+
631
668
  .hud-panel[data-minimized='true'] {
632
669
  overflow: hidden;
633
670
  padding-bottom: 12px;
@@ -649,6 +686,24 @@ button {
649
686
  white-space: nowrap;
650
687
  }
651
688
 
689
+ .hud-panel-planned .hud-panel-chrome-title {
690
+ color: #e7ebf2;
691
+ font-size: 14px;
692
+ letter-spacing: 0.02em;
693
+ text-transform: none;
694
+ }
695
+
696
+ .hud-panel-chrome-subtitle {
697
+ overflow: hidden;
698
+ color: #8b95a5;
699
+ font-size: 10px;
700
+ font-weight: 600;
701
+ letter-spacing: 0.08em;
702
+ text-transform: uppercase;
703
+ text-overflow: ellipsis;
704
+ white-space: nowrap;
705
+ }
706
+
652
707
  .hud-mode-switch {
653
708
  display: flex;
654
709
  align-items: center;
@@ -874,24 +929,28 @@ button {
874
929
  min-width: 0;
875
930
  }
876
931
 
877
- .hud-steps li[data-next='true'] {
932
+ .hud-steps li[data-active='true']:not([data-done='true']) {
878
933
  color: #ffffff;
879
934
  }
880
935
 
881
- .hud-steps li[data-next='true'] .hud-step-title {
936
+ .hud-steps li[data-active='true']:not([data-done='true']) .hud-step-title {
882
937
  color: #ffffff;
883
938
  text-decoration: underline;
884
939
  text-underline-offset: 3px;
885
940
  }
886
941
 
887
- .hud-steps li[data-current='true']:not([data-next='true']):not([data-done='true']) {
888
- color: #9ad8ff;
889
- }
890
-
891
942
  .hud-steps li[data-done='true'],
892
943
  .hud-steps li[data-done='true'] .hud-step-title {
893
944
  color: #3dff78;
894
945
  font-weight: 600;
946
+ }
947
+
948
+ .hud-steps li[data-done='true'][data-active='true'] .hud-step-title {
949
+ text-decoration: underline;
950
+ text-underline-offset: 3px;
951
+ }
952
+
953
+ .hud-steps li[data-done='true']:not([data-active='true']) .hud-step-title {
895
954
  text-decoration: none;
896
955
  }
897
956
 
@@ -922,6 +981,52 @@ button {
922
981
  font-size: 11px;
923
982
  }
924
983
 
984
+ .hud-setup {
985
+ display: grid;
986
+ gap: 16px;
987
+ margin-top: 12px;
988
+ }
989
+
990
+ .hud-setup-heading {
991
+ display: flex;
992
+ align-items: center;
993
+ flex-wrap: wrap;
994
+ gap: 8px;
995
+ margin: 0 0 8px;
996
+ color: #8b95a5;
997
+ font-size: 11px;
998
+ font-weight: 600;
999
+ letter-spacing: 0.08em;
1000
+ text-transform: uppercase;
1001
+ }
1002
+
1003
+ .hud-setup-tag {
1004
+ padding: 2px 6px;
1005
+ border: 1px solid #3a4250;
1006
+ border-radius: 3px;
1007
+ color: #8b95a5;
1008
+ font-size: 10px;
1009
+ font-weight: 600;
1010
+ letter-spacing: 0.04em;
1011
+ text-transform: none;
1012
+ }
1013
+
1014
+ .hud-setup-tag[data-ready='true'] {
1015
+ border-color: #5d9ec4;
1016
+ color: #d7eef8;
1017
+ }
1018
+
1019
+ .hud-setup-section p {
1020
+ margin: 0;
1021
+ color: #b7c0ce;
1022
+ font-size: 12px;
1023
+ line-height: 1.45;
1024
+ }
1025
+
1026
+ .hud-setup-section .hud-instruction {
1027
+ margin-top: 0;
1028
+ }
1029
+
925
1030
  .hud-instruction textarea {
926
1031
  width: 100%;
927
1032
  resize: vertical;
@@ -947,6 +1052,43 @@ button {
947
1052
  font-size: 12px;
948
1053
  }
949
1054
 
1055
+ .hud-live {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ flex-wrap: wrap;
1059
+ gap: 9px;
1060
+ margin: 2px 0 10px;
1061
+ padding: 8px 10px;
1062
+ border-radius: 8px;
1063
+ background: rgba(154, 216, 255, 0.08);
1064
+ border: 1px solid rgba(154, 216, 255, 0.22);
1065
+ color: #d7eef8;
1066
+ font-size: 12px;
1067
+ }
1068
+
1069
+ .hud-live[data-busy='false'] {
1070
+ background: rgba(255, 255, 255, 0.04);
1071
+ border-color: rgba(255, 255, 255, 0.12);
1072
+ color: #c5d0d8;
1073
+ }
1074
+
1075
+ .hud-live[data-flash='true'] {
1076
+ animation: hud-live-flash 0.65s ease;
1077
+ }
1078
+
1079
+ .hud-live .hud-button {
1080
+ margin-left: auto;
1081
+ }
1082
+
1083
+ @keyframes hud-live-flash {
1084
+ from {
1085
+ background: rgba(154, 216, 255, 0.32);
1086
+ }
1087
+ to {
1088
+ background: rgba(154, 216, 255, 0.08);
1089
+ }
1090
+ }
1091
+
950
1092
  .hud-working .hud-button {
951
1093
  margin-left: auto;
952
1094
  }
@@ -1,5 +1,6 @@
1
- import { Suspense } from 'react'
1
+ import { Suspense, useRef } from 'react'
2
2
  import { Billboard, Edges, Html, Text } from '@react-three/drei'
3
+ import { useFrame, useThree } from '@react-three/fiber'
3
4
  import { CHANGE_HIGHLIGHT, CONFIG, dimColor, fileColor, FILE_SELECTION, MAP_SELECTION, type ChangeKind } from '../theme'
4
5
  import { NameInput } from '../ui/NameInput'
5
6
  import { MapSelectBorder } from './MapSelectBorder'
@@ -40,6 +41,50 @@ function fileLabel(name: string, changeKind: ChangeKind | null, added: boolean)
40
41
  return name
41
42
  }
42
43
 
44
+ function changeMark(changeKind: ChangeKind | null, added: boolean) {
45
+ if (changeKind === 'remove') return 'D'
46
+ if (changeKind === 'add' || added) return 'A'
47
+ if (changeKind === 'edit') return 'U'
48
+ return null
49
+ }
50
+
51
+ function MapChangeMark({
52
+ mark,
53
+ width,
54
+ depth,
55
+ height,
56
+ }: {
57
+ mark: string
58
+ width: number
59
+ depth: number
60
+ height: number
61
+ }) {
62
+ const camera = useThree((state) => state.camera)
63
+ const markRef = useRef<HTMLDivElement>(null)
64
+
65
+ useFrame(() => {
66
+ const el = markRef.current
67
+ if (!el) return
68
+ const zoom = 'zoom' in camera ? Number(camera.zoom) : 1
69
+ const px = Math.min(width, depth) * 0.78 * zoom
70
+ el.style.fontSize = `${Math.max(px, 1)}px`
71
+ })
72
+
73
+ return (
74
+ <Html
75
+ position={[0, height / 2 + 0.12, 0]}
76
+ center
77
+ occlude={false}
78
+ zIndexRange={[120, 100]}
79
+ style={{ pointerEvents: 'none' }}
80
+ >
81
+ <div ref={markRef} className="map-change-mark">
82
+ {mark}
83
+ </div>
84
+ </Html>
85
+ )
86
+ }
87
+
43
88
  export function FileBlock({
44
89
  file,
45
90
  placed,
@@ -63,9 +108,11 @@ export function FileBlock({
63
108
  changeKind && highlightMapChange && !selected
64
109
  ? CHANGE_HIGHLIGHT[changeKind]
65
110
  : null
66
- const color = added || file.userCreated ? '#7ec8e8' : fileColor(file.language)
111
+ const isAdded = added || Boolean(file.userCreated)
112
+ const color = isAdded ? '#7ec8e8' : fileColor(file.language)
67
113
  const muted = dimColor(color, 0.32)
68
- const label = fileLabel(file.name, changeKind, added || Boolean(file.userCreated))
114
+ const label = fileLabel(file.name, changeKind, isAdded)
115
+ const mark = changeMark(changeKind, isAdded)
69
116
  const labelColor = aimed
70
117
  ? '#9ad8ff'
71
118
  : highlight
@@ -105,7 +152,7 @@ export function FileBlock({
105
152
  ? FILE_SELECTION.emissive
106
153
  : related
107
154
  ? '#1f4a44'
108
- : added || file.userCreated
155
+ : isAdded
109
156
  ? '#2a5064'
110
157
  : dimmed
111
158
  ? muted
@@ -120,7 +167,7 @@ export function FileBlock({
120
167
  ? 0.55
121
168
  : related
122
169
  ? 0.18
123
- : added || file.userCreated
170
+ : isAdded
124
171
  ? 0.22
125
172
  : dimmed
126
173
  ? 0.08
@@ -148,6 +195,9 @@ export function FileBlock({
148
195
  userData={{ fileId: file.id }}
149
196
  />
150
197
  )}
198
+ {mapMode && !naming && mark && (
199
+ <MapChangeMark mark={mark} width={width} depth={depth} height={height} />
200
+ )}
151
201
  {naming && onCommitName && onCancelName && (
152
202
  <Html
153
203
  position={[0, height / 2 + 0.42, 0]}
@@ -114,6 +114,7 @@ export type UserContext = {
114
114
  z: number
115
115
  }
116
116
  followLook?: boolean
117
+ showBranchChanges?: boolean
117
118
  userCreatedBlocks?: UserCreatedBlock[]
118
119
  userCreatedIslands?: UserCreatedIsland[]
119
120
  }
@@ -160,6 +161,40 @@ export function canStopSession(intent: {
160
161
  return Boolean(intent.sessionId) && intent.status !== 'idle'
161
162
  }
162
163
 
164
+ export function llmIsMakingChanges(intent: {
165
+ working: boolean
166
+ preview: boolean
167
+ status: AgentIntentStatus
168
+ }) {
169
+ return (
170
+ intent.working ||
171
+ intent.preview ||
172
+ intent.status === 'preparing' ||
173
+ intent.status === 'working' ||
174
+ intent.status === 'replanning' ||
175
+ intent.status === 'pending' ||
176
+ intent.status === 'extend' ||
177
+ intent.status === 'extended'
178
+ )
179
+ }
180
+
181
+ export type BranchChanges = {
182
+ available: boolean
183
+ branch: string | null
184
+ base: string | null
185
+ files: string[]
186
+ creates: string[]
187
+ deletes: string[]
188
+ createFolders: string[]
189
+ createLines: Record<string, number>
190
+ imports: PatchImport[]
191
+ addedFunctions: PatchSymbolAddition[]
192
+ addedVariables: PatchSymbolAddition[]
193
+ addedImports: PatchImportAddition[]
194
+ changedFunctions: PatchSymbolAddition[]
195
+ changedVariables: PatchSymbolAddition[]
196
+ }
197
+
163
198
  export type PlanStep = {
164
199
  index: number
165
200
  title: string
@@ -215,6 +250,7 @@ export type WorkflowAction =
215
250
  | 'blueprint_no'
216
251
  | 'blueprint_send'
217
252
  | 'blueprint_update'
253
+ | 'focus'
218
254
  | 'set_step_by_step'
219
255
 
220
256
  export type AgentIntentBundle = {
@@ -226,6 +262,7 @@ export type AgentIntent = {
226
262
  updatedAt: string | null
227
263
  showMap: boolean
228
264
  status: AgentIntentStatus
265
+ name: string | null
229
266
  feature: string | null
230
267
  steps: PlanStep[]
231
268
  step: number | null
@@ -252,6 +289,15 @@ export type AgentIntent = {
252
289
  phase: WorkflowPhase | null
253
290
  working: boolean
254
291
  stalledWait: boolean
292
+ llmIdle?: boolean
293
+ awaitingAttach?: boolean
294
+ listening?: boolean
295
+ lastAck?: {
296
+ kind: string
297
+ detail: string
298
+ at: string | null
299
+ } | null
300
+ initialInstruction?: string | null
255
301
  creationMode: boolean
256
302
  canEnterBlueprint: boolean
257
303
  blueprintSessionId: string | null