@neurocode-ai/session-ui 1.18.8
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.
- package/package.json +71 -0
- package/src/components/apply-patch-file.test.ts +43 -0
- package/src/components/apply-patch-file.ts +78 -0
- package/src/components/basic-tool.css +358 -0
- package/src/components/basic-tool.stories.tsx +133 -0
- package/src/components/basic-tool.tsx +343 -0
- package/src/components/dock-prompt.stories.tsx +62 -0
- package/src/components/dock-prompt.tsx +23 -0
- package/src/components/file-media.tsx +291 -0
- package/src/components/file-search.tsx +72 -0
- package/src/components/file-ssr.tsx +197 -0
- package/src/components/file.css +46 -0
- package/src/components/file.tsx +1202 -0
- package/src/components/line-comment-annotations.tsx +637 -0
- package/src/components/line-comment-styles.ts +292 -0
- package/src/components/line-comment.stories.tsx +115 -0
- package/src/components/line-comment.tsx +439 -0
- package/src/components/markdown-cache.tsx +78 -0
- package/src/components/markdown-code-state.test.ts +32 -0
- package/src/components/markdown-code-state.ts +22 -0
- package/src/components/markdown-inline-code-kind.test.ts +46 -0
- package/src/components/markdown-inline-code-kind.ts +1915 -0
- package/src/components/markdown-preload.test.ts +18 -0
- package/src/components/markdown-shiki.worker.ts +104 -0
- package/src/components/markdown-stream.test.ts +194 -0
- package/src/components/markdown-stream.ts +110 -0
- package/src/components/markdown-worker-protocol.test.ts +81 -0
- package/src/components/markdown-worker-protocol.ts +48 -0
- package/src/components/markdown-worker-queue.test.ts +49 -0
- package/src/components/markdown-worker-queue.ts +64 -0
- package/src/components/markdown-worker-transport.test.ts +56 -0
- package/src/components/markdown-worker-transport.ts +41 -0
- package/src/components/markdown-worker.ts +122 -0
- package/src/components/markdown.css +377 -0
- package/src/components/markdown.stories.tsx +53 -0
- package/src/components/markdown.tsx +678 -0
- package/src/components/message-file.test.ts +63 -0
- package/src/components/message-file.ts +34 -0
- package/src/components/message-nav.css +127 -0
- package/src/components/message-nav.stories.tsx +7 -0
- package/src/components/message-nav.tsx +102 -0
- package/src/components/message-part-text.ts +3 -0
- package/src/components/message-part.css +1569 -0
- package/src/components/message-part.stories.tsx +7 -0
- package/src/components/message-part.test.ts +28 -0
- package/src/components/message-part.tsx +2662 -0
- package/src/components/session-diff.test.ts +135 -0
- package/src/components/session-diff.ts +145 -0
- package/src/components/session-retry.tsx +74 -0
- package/src/components/session-review.css +247 -0
- package/src/components/session-review.stories.tsx +7 -0
- package/src/components/session-review.tsx +657 -0
- package/src/components/session-turn.css +241 -0
- package/src/components/session-turn.stories.tsx +7 -0
- package/src/components/session-turn.tsx +543 -0
- package/src/components/shell-submessage-motion.stories.tsx +346 -0
- package/src/components/shell-submessage.css +23 -0
- package/src/components/timeline-playground.stories.tsx +2090 -0
- package/src/components/tool-count-label.css +57 -0
- package/src/components/tool-count-label.tsx +58 -0
- package/src/components/tool-count-summary.css +102 -0
- package/src/components/tool-count-summary.stories.tsx +238 -0
- package/src/components/tool-count-summary.tsx +52 -0
- package/src/components/tool-error-card.css +91 -0
- package/src/components/tool-error-card.stories.tsx +92 -0
- package/src/components/tool-error-card.tsx +157 -0
- package/src/components/tool-status-title.css +89 -0
- package/src/components/tool-status-title.tsx +133 -0
- package/src/context/data.tsx +64 -0
- package/src/context/index.ts +1 -0
- package/src/pierre/comment-hover.ts +83 -0
- package/src/pierre/commented-lines.ts +91 -0
- package/src/pierre/diff-selection.ts +71 -0
- package/src/pierre/file-find.ts +485 -0
- package/src/pierre/file-runtime.ts +114 -0
- package/src/pierre/file-selection.ts +85 -0
- package/src/pierre/index.ts +186 -0
- package/src/pierre/media.ts +110 -0
- package/src/pierre/selection-bridge.ts +132 -0
- package/src/pierre/virtualizer.ts +100 -0
- package/src/pierre/worker.ts +52 -0
- package/src/styles/index.css +14 -0
- package/src/v2/components/attachment-card-v2.css +70 -0
- package/src/v2/components/attachment-card-v2.tsx +33 -0
- package/src/v2/components/basic-tool-v2.css +163 -0
- package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
- package/src/v2/components/basic-tool-v2.tsx +139 -0
- package/src/v2/components/comment-card-v2.tsx +64 -0
- package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
- package/src/v2/components/prompt-input/attachments.ts +266 -0
- package/src/v2/components/prompt-input/index.tsx +706 -0
- package/src/v2/components/prompt-input/interaction.ts +482 -0
- package/src/v2/components/prompt-input/machine.test.ts +164 -0
- package/src/v2/components/prompt-input/machine.ts +261 -0
- package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
- package/src/v2/components/prompt-input/store.test.ts +116 -0
- package/src/v2/components/prompt-input/store.ts +152 -0
- package/src/v2/components/prompt-input/types.ts +106 -0
- package/src/v2/components/session-file-panel-v2.tsx +43 -0
- package/src/v2/components/session-progress-indicator-v2.css +875 -0
- package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
- package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
- package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
- package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
- package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
- package/src/v2/components/session-review-v2.css +458 -0
- package/src/v2/components/session-review-v2.tsx +340 -0
- package/src/v2/components/tool-error-card-v2.css +200 -0
- package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
- package/src/v2/components/tool-error-card-v2.tsx +166 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { PromptInputV2HistoryEntry, PromptInputV2PersistedState, PromptInputV2Suggestion } from "./types"
|
|
2
|
+
|
|
3
|
+
export type PromptInputV2InteractionState = {
|
|
4
|
+
mode: "normal" | "shell"
|
|
5
|
+
popover:
|
|
6
|
+
| { type: "closed" }
|
|
7
|
+
| { type: "context"; query: string; activeID?: string }
|
|
8
|
+
| { type: "command-inline"; query: string; activeID?: string }
|
|
9
|
+
| { type: "command-menu"; query: string; activeID?: string }
|
|
10
|
+
drag: "idle" | "active"
|
|
11
|
+
focus: "editor" | "command-search" | "external"
|
|
12
|
+
activeContextID?: string
|
|
13
|
+
historyIndex: number
|
|
14
|
+
savedHistory?: PromptInputV2HistoryEntry
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PromptInputV2InteractionEvent =
|
|
18
|
+
| { type: "input.changed"; value: string; persist?: boolean }
|
|
19
|
+
| { type: "commands.open" }
|
|
20
|
+
| { type: "context.open" }
|
|
21
|
+
| { type: "popover.query"; value: string }
|
|
22
|
+
| { type: "popover.results"; ids: string[] }
|
|
23
|
+
| { type: "popover.active"; id: string }
|
|
24
|
+
| { type: "popover.close" }
|
|
25
|
+
| { type: "popover.select"; item: PromptInputV2Suggestion }
|
|
26
|
+
| { type: "key.down"; key: string; ctrl: boolean; composing: boolean; ids: string[]; empty?: boolean }
|
|
27
|
+
| { type: "mode.shell" }
|
|
28
|
+
| { type: "mode.normal" }
|
|
29
|
+
| { type: "drag.enter" }
|
|
30
|
+
| { type: "drag.leave" }
|
|
31
|
+
| { type: "focus.editor" }
|
|
32
|
+
| { type: "focus.external" }
|
|
33
|
+
| { type: "context.active"; id: string }
|
|
34
|
+
|
|
35
|
+
export type PromptInputV2InteractionCommand =
|
|
36
|
+
| { type: "draft.setText"; value: string }
|
|
37
|
+
| { type: "mention.add"; item: PromptInputV2Suggestion }
|
|
38
|
+
| { type: "popover.filter"; popover: "command" | "context"; query: string }
|
|
39
|
+
| { type: "suggestion.select"; id: string }
|
|
40
|
+
| { type: "focus.editor" }
|
|
41
|
+
| { type: "focus.command-search" }
|
|
42
|
+
|
|
43
|
+
export type PromptInputV2Transition = {
|
|
44
|
+
state: PromptInputV2InteractionState
|
|
45
|
+
commands: PromptInputV2InteractionCommand[]
|
|
46
|
+
handled: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createPromptInputV2InteractionState(): PromptInputV2InteractionState {
|
|
50
|
+
return {
|
|
51
|
+
mode: "normal",
|
|
52
|
+
popover: { type: "closed" },
|
|
53
|
+
drag: "idle",
|
|
54
|
+
focus: "external",
|
|
55
|
+
historyIndex: -1,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function transitionPromptInputV2(
|
|
60
|
+
state: PromptInputV2InteractionState,
|
|
61
|
+
event: PromptInputV2InteractionEvent,
|
|
62
|
+
persisted: PromptInputV2PersistedState,
|
|
63
|
+
): PromptInputV2Transition {
|
|
64
|
+
if (event.type === "input.changed") return inputChanged(state, event.value, event.persist !== false, persisted.cursor)
|
|
65
|
+
if (event.type === "commands.open") return openCommands(state, persisted)
|
|
66
|
+
if (event.type === "context.open") return openContext(state, persisted)
|
|
67
|
+
if (event.type === "popover.query") return queryChanged(state, event.value)
|
|
68
|
+
if (event.type === "popover.results") return resultsChanged(state, event.ids)
|
|
69
|
+
if (event.type === "popover.active") return activeChanged(state, event.id)
|
|
70
|
+
if (event.type === "popover.close") return changed({ ...state, popover: { type: "closed" } })
|
|
71
|
+
if (event.type === "popover.select") return suggestionSelected(state, event.item, persisted)
|
|
72
|
+
if (event.type === "key.down") return keyDown(state, event)
|
|
73
|
+
if (event.type === "mode.shell") return changed({ ...state, mode: "shell", popover: { type: "closed" } })
|
|
74
|
+
if (event.type === "mode.normal") return changed({ ...state, mode: "normal" })
|
|
75
|
+
if (event.type === "drag.enter") return changed({ ...state, drag: "active" })
|
|
76
|
+
if (event.type === "drag.leave") return changed({ ...state, drag: "idle" })
|
|
77
|
+
if (event.type === "focus.editor") return changed({ ...state, focus: "editor" })
|
|
78
|
+
if (event.type === "context.active") {
|
|
79
|
+
return changed({ ...state, activeContextID: state.activeContextID === event.id ? undefined : event.id })
|
|
80
|
+
}
|
|
81
|
+
return changed({ ...state, focus: "external" })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inputChanged(
|
|
85
|
+
state: PromptInputV2InteractionState,
|
|
86
|
+
value: string,
|
|
87
|
+
persist: boolean,
|
|
88
|
+
cursor: number | undefined,
|
|
89
|
+
): PromptInputV2Transition {
|
|
90
|
+
const setText: PromptInputV2InteractionCommand[] = persist ? [{ type: "draft.setText", value }] : []
|
|
91
|
+
if (state.mode === "normal" && value === "!") {
|
|
92
|
+
return changed({ ...state, mode: "shell", popover: { type: "closed" }, focus: "editor" }, [
|
|
93
|
+
{ type: "draft.setText", value: "" },
|
|
94
|
+
])
|
|
95
|
+
}
|
|
96
|
+
const context = value.slice(0, cursor ?? value.length).match(/(?:^|\s)@([^\s@]*)$/)
|
|
97
|
+
if (context) {
|
|
98
|
+
const query = context[1] ?? ""
|
|
99
|
+
return changed({ ...state, popover: { type: "context", query }, focus: "editor" }, [
|
|
100
|
+
...setText,
|
|
101
|
+
{ type: "popover.filter", popover: "context", query },
|
|
102
|
+
])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const command = value.match(/^\/(\S*)$/)
|
|
106
|
+
if (command) {
|
|
107
|
+
const query = command[1] ?? ""
|
|
108
|
+
return changed({ ...state, popover: { type: "command-inline", query }, focus: "editor" }, [
|
|
109
|
+
...setText,
|
|
110
|
+
{ type: "popover.filter", popover: "command", query },
|
|
111
|
+
])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return changed(
|
|
115
|
+
{ ...state, popover: state.popover.type === "command-menu" ? state.popover : { type: "closed" }, focus: "editor" },
|
|
116
|
+
setText,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function openCommands(
|
|
121
|
+
state: PromptInputV2InteractionState,
|
|
122
|
+
persisted: PromptInputV2PersistedState,
|
|
123
|
+
): PromptInputV2Transition {
|
|
124
|
+
if (!populated(persisted)) {
|
|
125
|
+
return changed({ ...state, popover: { type: "command-inline", query: "" }, focus: "editor" }, [
|
|
126
|
+
{ type: "draft.setText", value: promptText(persisted) + "/" },
|
|
127
|
+
{ type: "popover.filter", popover: "command", query: "" },
|
|
128
|
+
{ type: "focus.editor" },
|
|
129
|
+
])
|
|
130
|
+
}
|
|
131
|
+
return changed({ ...state, popover: { type: "command-menu", query: "" }, focus: "command-search" }, [
|
|
132
|
+
{ type: "popover.filter", popover: "command", query: "" },
|
|
133
|
+
{ type: "focus.command-search" },
|
|
134
|
+
])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function openContext(
|
|
138
|
+
state: PromptInputV2InteractionState,
|
|
139
|
+
persisted: PromptInputV2PersistedState,
|
|
140
|
+
): PromptInputV2Transition {
|
|
141
|
+
return changed({ ...state, popover: { type: "context", query: "" }, focus: "editor" }, [
|
|
142
|
+
{ type: "draft.setText", value: promptText(persisted) + "@" },
|
|
143
|
+
{ type: "popover.filter", popover: "context", query: "" },
|
|
144
|
+
{ type: "focus.editor" },
|
|
145
|
+
])
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function queryChanged(state: PromptInputV2InteractionState, query: string): PromptInputV2Transition {
|
|
149
|
+
if (state.popover.type === "closed") return unchanged(state)
|
|
150
|
+
const popover = state.popover.type === "context" ? "context" : "command"
|
|
151
|
+
return changed({ ...state, popover: { ...state.popover, query, activeID: undefined } }, [
|
|
152
|
+
{ type: "popover.filter", popover, query },
|
|
153
|
+
])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resultsChanged(state: PromptInputV2InteractionState, ids: string[]): PromptInputV2Transition {
|
|
157
|
+
if (state.popover.type === "closed") return unchanged(state)
|
|
158
|
+
const activeID = state.popover.activeID && ids.includes(state.popover.activeID) ? state.popover.activeID : ids[0]
|
|
159
|
+
if (activeID === state.popover.activeID) return unchanged(state)
|
|
160
|
+
return changed({ ...state, popover: { ...state.popover, activeID } })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function activeChanged(state: PromptInputV2InteractionState, id: string): PromptInputV2Transition {
|
|
164
|
+
if (state.popover.type === "closed" || state.popover.activeID === id) return unchanged(state)
|
|
165
|
+
return changed({ ...state, popover: { ...state.popover, activeID: id } })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function suggestionSelected(
|
|
169
|
+
state: PromptInputV2InteractionState,
|
|
170
|
+
item: PromptInputV2Suggestion,
|
|
171
|
+
persisted: PromptInputV2PersistedState,
|
|
172
|
+
): PromptInputV2Transition {
|
|
173
|
+
const current = promptText(persisted)
|
|
174
|
+
const commands: PromptInputV2InteractionCommand[] = []
|
|
175
|
+
if (item.kind === "command") {
|
|
176
|
+
commands.push({
|
|
177
|
+
type: "draft.setText",
|
|
178
|
+
value:
|
|
179
|
+
state.popover.type === "command-menu"
|
|
180
|
+
? current.trim()
|
|
181
|
+
? `${item.label} ${current.trim()}`
|
|
182
|
+
: `${item.label} `
|
|
183
|
+
: replaceTrigger(current, "/", `${item.label} `),
|
|
184
|
+
})
|
|
185
|
+
} else {
|
|
186
|
+
commands.push({ type: "mention.add", item })
|
|
187
|
+
}
|
|
188
|
+
commands.push({ type: "focus.editor" })
|
|
189
|
+
return changed({ ...state, popover: { type: "closed" }, focus: "editor" }, commands)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function keyDown(
|
|
193
|
+
state: PromptInputV2InteractionState,
|
|
194
|
+
event: Extract<PromptInputV2InteractionEvent, { type: "key.down" }>,
|
|
195
|
+
): PromptInputV2Transition {
|
|
196
|
+
if (event.ctrl && event.key.toLowerCase() === "g") {
|
|
197
|
+
if (state.popover.type === "closed") return unchanged(state)
|
|
198
|
+
return changed({ ...state, popover: { type: "closed" }, focus: "editor" }, [{ type: "focus.editor" }], true)
|
|
199
|
+
}
|
|
200
|
+
if (state.popover.type === "closed") {
|
|
201
|
+
if (state.mode === "shell" && (event.key === "Escape" || (event.key === "Backspace" && event.empty))) {
|
|
202
|
+
return changed({ ...state, mode: "normal" }, [], true)
|
|
203
|
+
}
|
|
204
|
+
return unchanged(state)
|
|
205
|
+
}
|
|
206
|
+
if (event.key === "Escape") {
|
|
207
|
+
return changed({ ...state, popover: { type: "closed" }, focus: "editor" }, [{ type: "focus.editor" }], true)
|
|
208
|
+
}
|
|
209
|
+
if (event.key === "Tab" || (event.key === "Enter" && !event.composing)) {
|
|
210
|
+
if (!state.popover.activeID) return unchanged(state, true)
|
|
211
|
+
return unchanged(state, true, [{ type: "suggestion.select", id: state.popover.activeID }])
|
|
212
|
+
}
|
|
213
|
+
const direction =
|
|
214
|
+
event.key === "ArrowDown" || (event.ctrl && event.key === "n")
|
|
215
|
+
? 1
|
|
216
|
+
: event.key === "ArrowUp" || (event.ctrl && event.key === "p")
|
|
217
|
+
? -1
|
|
218
|
+
: 0
|
|
219
|
+
if (!direction || event.ids.length === 0) return unchanged(state)
|
|
220
|
+
const current = state.popover.activeID ? event.ids.indexOf(state.popover.activeID) : -1
|
|
221
|
+
const index =
|
|
222
|
+
current < 0
|
|
223
|
+
? direction === 1
|
|
224
|
+
? 0
|
|
225
|
+
: event.ids.length - 1
|
|
226
|
+
: (current + direction + event.ids.length) % event.ids.length
|
|
227
|
+
return changed({ ...state, popover: { ...state.popover, activeID: event.ids[index] } }, [], true)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function promptText(persisted: PromptInputV2PersistedState) {
|
|
231
|
+
return persisted.prompt.map((part) => (part.type === "text" ? part.content : "")).join("")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function populated(persisted: PromptInputV2PersistedState) {
|
|
235
|
+
return (
|
|
236
|
+
!!promptText(persisted).trim() ||
|
|
237
|
+
persisted.context.items.length > 0 ||
|
|
238
|
+
persisted.prompt.some((part) => part.type === "file" || part.type === "image")
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function replaceTrigger(value: string, trigger: "@" | "/", replacement: string) {
|
|
243
|
+
const index = trigger === "/" ? value.indexOf(trigger) : value.lastIndexOf(trigger)
|
|
244
|
+
return index < 0 ? replacement : value.slice(0, index) + replacement
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function changed(
|
|
248
|
+
state: PromptInputV2InteractionState,
|
|
249
|
+
commands: PromptInputV2InteractionCommand[] = [],
|
|
250
|
+
handled = false,
|
|
251
|
+
): PromptInputV2Transition {
|
|
252
|
+
return { state, commands, handled }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function unchanged(
|
|
256
|
+
state: PromptInputV2InteractionState,
|
|
257
|
+
handled = false,
|
|
258
|
+
commands: PromptInputV2InteractionCommand[] = [],
|
|
259
|
+
): PromptInputV2Transition {
|
|
260
|
+
return { state, commands, handled }
|
|
261
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { PromptInputV2, type PromptInputV2PersistedState, type PromptInputV2Suggestion } from "."
|
|
4
|
+
import { createPromptInputV2Controller } from "./interaction"
|
|
5
|
+
import { createPromptInputV2Store } from "./store"
|
|
6
|
+
import { createEffect } from "solid-js"
|
|
7
|
+
|
|
8
|
+
const agents = [
|
|
9
|
+
{ id: "build", label: "Build" },
|
|
10
|
+
{ id: "plan", label: "Plan" },
|
|
11
|
+
{ id: "review", label: "Review" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const variants = [
|
|
15
|
+
{ id: "default", label: "Default" },
|
|
16
|
+
{ id: "fast", label: "Fast" },
|
|
17
|
+
{ id: "thinking", label: "Thinking" },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const models = [
|
|
21
|
+
{ id: "claude-sonnet", name: "Claude Sonnet", providerID: "anthropic" },
|
|
22
|
+
{ id: "gpt-5", name: "GPT-5", providerID: "openai" },
|
|
23
|
+
{ id: "gemini-pro", name: "Gemini Pro", providerID: "google" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const contextSuggestions: PromptInputV2Suggestion[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "file-prompt",
|
|
29
|
+
kind: "file",
|
|
30
|
+
label: "prompt-input-v2.tsx",
|
|
31
|
+
path: "src/components/prompt-input-v2.tsx",
|
|
32
|
+
recent: true,
|
|
33
|
+
mention: {
|
|
34
|
+
type: "file",
|
|
35
|
+
path: "src/components/prompt-input-v2.tsx",
|
|
36
|
+
content: "@src/components/prompt-input-v2.tsx",
|
|
37
|
+
start: 0,
|
|
38
|
+
end: 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "file-story",
|
|
43
|
+
kind: "file",
|
|
44
|
+
label: "prompt-input-v2.stories.tsx",
|
|
45
|
+
path: "src/components/prompt-input-v2.stories.tsx",
|
|
46
|
+
mention: {
|
|
47
|
+
type: "file",
|
|
48
|
+
path: "src/components/prompt-input-v2.stories.tsx",
|
|
49
|
+
content: "@src/components/prompt-input-v2.stories.tsx",
|
|
50
|
+
start: 0,
|
|
51
|
+
end: 0,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "agent-review",
|
|
56
|
+
kind: "agent",
|
|
57
|
+
label: "@review",
|
|
58
|
+
description: "Ask the review agent",
|
|
59
|
+
mention: { type: "agent", name: "review", content: "@review", start: 0, end: 0 },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "reference-docs",
|
|
63
|
+
kind: "reference",
|
|
64
|
+
label: "@UI guidelines",
|
|
65
|
+
path: "docs/ui.md",
|
|
66
|
+
description: "Project reference",
|
|
67
|
+
mention: {
|
|
68
|
+
type: "file",
|
|
69
|
+
path: "docs/ui.md",
|
|
70
|
+
content: "@UI guidelines",
|
|
71
|
+
start: 0,
|
|
72
|
+
end: 0,
|
|
73
|
+
mime: "application/x-directory",
|
|
74
|
+
filename: "UI guidelines",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
const commandSuggestions: PromptInputV2Suggestion[] = [
|
|
80
|
+
{
|
|
81
|
+
id: "command-fix",
|
|
82
|
+
kind: "command",
|
|
83
|
+
label: "/fix",
|
|
84
|
+
trigger: "fix",
|
|
85
|
+
title: "Fix",
|
|
86
|
+
description: "Fix the current issue",
|
|
87
|
+
keybind: ["Enter"],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "command-review",
|
|
91
|
+
kind: "command",
|
|
92
|
+
label: "/review",
|
|
93
|
+
trigger: "review",
|
|
94
|
+
title: "Review",
|
|
95
|
+
description: "Review pending changes",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "command-test",
|
|
99
|
+
kind: "command",
|
|
100
|
+
label: "/test",
|
|
101
|
+
trigger: "test",
|
|
102
|
+
title: "Test",
|
|
103
|
+
description: "Run relevant tests",
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
function ControlledPromptInput() {
|
|
108
|
+
// Agent choice is a persisted user/workspace preference in v1, not part of PromptStore.
|
|
109
|
+
const [preferences, setPreferences] = createStore({ agent: "build" })
|
|
110
|
+
|
|
111
|
+
const [runtime, setRuntime] = createStore({
|
|
112
|
+
stopping: false,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// This matches the v1 PromptStore and can use the same persistence boundary.
|
|
116
|
+
const state = createStore<PromptInputV2PersistedState>({
|
|
117
|
+
prompt: [
|
|
118
|
+
{ type: "text", content: "", start: 0, end: 0 },
|
|
119
|
+
{
|
|
120
|
+
type: "image",
|
|
121
|
+
id: "attachment-1",
|
|
122
|
+
filename: "requirements.md",
|
|
123
|
+
mime: "text/markdown",
|
|
124
|
+
dataUrl: "data:text/markdown;base64,IyBSZXF1aXJlbWVudHM=",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
cursor: 0,
|
|
128
|
+
model: { providerID: "anthropic", modelID: "claude-sonnet", variant: null },
|
|
129
|
+
context: {
|
|
130
|
+
items: [
|
|
131
|
+
{
|
|
132
|
+
key: "file:src/components/prompt-input-v2.tsx:1:40",
|
|
133
|
+
type: "file",
|
|
134
|
+
path: "src/components/prompt-input-v2.tsx",
|
|
135
|
+
selection: { startLine: 1, startChar: 0, endLine: 40, endChar: 0 },
|
|
136
|
+
comment: "Keep this component context-free",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
const store = createPromptInputV2Store(state)
|
|
142
|
+
|
|
143
|
+
const controller = createPromptInputV2Controller({
|
|
144
|
+
store: state,
|
|
145
|
+
commands: () => commandSuggestions,
|
|
146
|
+
context: () => contextSuggestions,
|
|
147
|
+
searchContextFiles: (query) => {
|
|
148
|
+
const needle = query.trim().toLowerCase()
|
|
149
|
+
return contextSuggestions.filter(
|
|
150
|
+
(item) => item.kind === "file" && `${item.label} ${item.path ?? ""}`.toLowerCase().includes(needle),
|
|
151
|
+
)
|
|
152
|
+
},
|
|
153
|
+
view: {
|
|
154
|
+
add: {
|
|
155
|
+
onAttach: () => addAttachment("architecture.txt", "text/plain"),
|
|
156
|
+
},
|
|
157
|
+
agent: {
|
|
158
|
+
options: () => agents,
|
|
159
|
+
current: () => preferences.agent,
|
|
160
|
+
onSelect: (agent) => setPreferences("agent", agent),
|
|
161
|
+
},
|
|
162
|
+
model: {
|
|
163
|
+
options: () => models.map((model) => ({ id: model.id, label: model.name, providerID: model.providerID })),
|
|
164
|
+
current: () =>
|
|
165
|
+
models.find(
|
|
166
|
+
(model) => model.id === store.state.model?.modelID && model.providerID === store.state.model?.providerID,
|
|
167
|
+
)?.id ?? "",
|
|
168
|
+
onSelect(id) {
|
|
169
|
+
const model = models.find((item) => item.id === id)
|
|
170
|
+
if (!model) return
|
|
171
|
+
store.setModel({
|
|
172
|
+
providerID: model.providerID,
|
|
173
|
+
modelID: model.id,
|
|
174
|
+
variant: store.state.model?.variant,
|
|
175
|
+
})
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
variant: {
|
|
179
|
+
options: () => variants,
|
|
180
|
+
current: () => store.state.model?.variant ?? "default",
|
|
181
|
+
onSelect: (variant) => store.setVariant(variant === "default" ? null : variant),
|
|
182
|
+
},
|
|
183
|
+
submit: {
|
|
184
|
+
stopping: () => runtime.stopping,
|
|
185
|
+
working: () => runtime.stopping,
|
|
186
|
+
onSubmit: () => {
|
|
187
|
+
store.reset()
|
|
188
|
+
setRuntime("stopping", true)
|
|
189
|
+
},
|
|
190
|
+
onStop: () => setRuntime("stopping", false),
|
|
191
|
+
},
|
|
192
|
+
onDrop: (event) => addAttachment(event.dataTransfer?.files[0]?.name ?? "dropped-file.txt", "text/plain"),
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const addAttachment = (filename: string, mime: string) => {
|
|
197
|
+
store.addAttachment({
|
|
198
|
+
type: "image",
|
|
199
|
+
id: `attachment-${store.state.prompt.filter((part) => part.type === "image").length + 1}`,
|
|
200
|
+
filename,
|
|
201
|
+
mime,
|
|
202
|
+
dataUrl: `data:${mime};base64,`,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div class="mx-auto flex max-w-[760px] flex-col gap-4 pt-32">
|
|
208
|
+
<PromptInputV2 controller={controller} />
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default {
|
|
214
|
+
title: "Session UI/PromptInputV2",
|
|
215
|
+
id: "session-ui-prompt-input-v2",
|
|
216
|
+
component: PromptInputV2,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const ControlledComposition = {
|
|
220
|
+
render: () => <ControlledPromptInput />,
|
|
221
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import type { PromptInputV2PersistedState } from "./types"
|
|
4
|
+
import { createPromptInputV2Store } from "./store"
|
|
5
|
+
|
|
6
|
+
function createPromptStore() {
|
|
7
|
+
return createPromptInputV2Store(
|
|
8
|
+
createStore<PromptInputV2PersistedState>({
|
|
9
|
+
prompt: [
|
|
10
|
+
{ type: "text", content: "old", start: 0, end: 3 },
|
|
11
|
+
{
|
|
12
|
+
type: "image",
|
|
13
|
+
id: "attachment-1",
|
|
14
|
+
filename: "notes.txt",
|
|
15
|
+
mime: "text/plain",
|
|
16
|
+
dataUrl: "data:text/plain;base64,",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
cursor: 3,
|
|
20
|
+
model: { providerID: "anthropic", modelID: "claude-sonnet", variant: null },
|
|
21
|
+
context: { items: [] },
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("prompt input v2 store", () => {
|
|
27
|
+
test("accepts an accessor for the backing store", () => {
|
|
28
|
+
const [state, setState] = createStore<PromptInputV2PersistedState>({
|
|
29
|
+
prompt: [{ type: "text", content: "", start: 0, end: 0 }],
|
|
30
|
+
cursor: 0,
|
|
31
|
+
context: { items: [] },
|
|
32
|
+
})
|
|
33
|
+
const prompt = createPromptInputV2Store([() => state, setState])
|
|
34
|
+
|
|
35
|
+
prompt.setText("accessed")
|
|
36
|
+
|
|
37
|
+
expect(prompt.state.prompt).toEqual([{ type: "text", content: "accessed", start: 0, end: 8 }])
|
|
38
|
+
expect(prompt.state.cursor).toBe(8)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("updates prompt text and cursor together while preserving attachments", () => {
|
|
42
|
+
const prompt = createPromptStore()
|
|
43
|
+
|
|
44
|
+
prompt.setText("updated")
|
|
45
|
+
|
|
46
|
+
expect(prompt.state.prompt).toEqual([
|
|
47
|
+
{ type: "text", content: "updated", start: 0, end: 7 },
|
|
48
|
+
{
|
|
49
|
+
type: "image",
|
|
50
|
+
id: "attachment-1",
|
|
51
|
+
filename: "notes.txt",
|
|
52
|
+
mime: "text/plain",
|
|
53
|
+
dataUrl: "data:text/plain;base64,",
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
expect(prompt.state.cursor).toBe(7)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("inserts text without flattening structured mentions", () => {
|
|
60
|
+
const [state, setState] = createStore<PromptInputV2PersistedState>({
|
|
61
|
+
prompt: [
|
|
62
|
+
{ type: "text", content: "A ", start: 0, end: 2 },
|
|
63
|
+
{ type: "file", path: "one", content: "@one", start: 2, end: 6 },
|
|
64
|
+
{ type: "text", content: " B", start: 6, end: 8 },
|
|
65
|
+
],
|
|
66
|
+
cursor: 2,
|
|
67
|
+
context: { items: [] },
|
|
68
|
+
})
|
|
69
|
+
const prompt = createPromptInputV2Store([state, setState])
|
|
70
|
+
|
|
71
|
+
prompt.addText("X\nY")
|
|
72
|
+
|
|
73
|
+
expect(prompt.state.prompt).toEqual([
|
|
74
|
+
{ type: "text", content: "A X\nY", start: 0, end: 5 },
|
|
75
|
+
{ type: "file", path: "one", content: "@one", start: 5, end: 9 },
|
|
76
|
+
{ type: "text", content: " B", start: 9, end: 11 },
|
|
77
|
+
])
|
|
78
|
+
expect(prompt.state.cursor).toBe(5)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("mutates context, attachments, and model through shared actions", () => {
|
|
82
|
+
const prompt = createPromptStore()
|
|
83
|
+
const context = { key: "file:src/index.ts", type: "file" as const, path: "src/index.ts" }
|
|
84
|
+
|
|
85
|
+
prompt.addContext(context)
|
|
86
|
+
prompt.addContext(context)
|
|
87
|
+
prompt.addMention({ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 0 })
|
|
88
|
+
prompt.removeAttachment("attachment-1")
|
|
89
|
+
prompt.setVariant("thinking")
|
|
90
|
+
|
|
91
|
+
expect(prompt.state.context.items).toEqual([context])
|
|
92
|
+
expect(prompt.state.prompt).toEqual([
|
|
93
|
+
{ type: "text", content: "old", start: 0, end: 3 },
|
|
94
|
+
{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 3, end: 14 },
|
|
95
|
+
{ type: "text", content: " ", start: 14, end: 15 },
|
|
96
|
+
])
|
|
97
|
+
expect(prompt.state.model?.variant).toBe("thinking")
|
|
98
|
+
|
|
99
|
+
prompt.removeContext(context.key)
|
|
100
|
+
prompt.setPrompt([{ type: "text", content: "old", start: 0, end: 3 }], 3)
|
|
101
|
+
prompt.setModel(undefined)
|
|
102
|
+
|
|
103
|
+
expect(prompt.state.context.items).toEqual([])
|
|
104
|
+
expect(prompt.state.prompt).toEqual([{ type: "text", content: "old", start: 0, end: 3 }])
|
|
105
|
+
expect(prompt.state.model).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("resets the prompt and cursor", () => {
|
|
109
|
+
const prompt = createPromptStore()
|
|
110
|
+
|
|
111
|
+
prompt.reset()
|
|
112
|
+
|
|
113
|
+
expect(prompt.state.prompt).toEqual([{ type: "text", content: "", start: 0, end: 0 }])
|
|
114
|
+
expect(prompt.state.cursor).toBe(0)
|
|
115
|
+
})
|
|
116
|
+
})
|