windmill-components 1.502.6 → 1.503.3

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.
Files changed (46) hide show
  1. package/package/actions/triggerableByAI.svelte.js +2 -0
  2. package/package/components/Editor.svelte +85 -9
  3. package/package/components/Editor.svelte.d.ts +0 -2
  4. package/package/components/FakeMonacoPlaceHolder.svelte +2 -1
  5. package/package/components/FlowWrapper.svelte +19 -4
  6. package/package/components/FlowWrapper.svelte.d.ts +4 -1
  7. package/package/components/JsonEditor.svelte +9 -2
  8. package/package/components/Path.svelte +2 -2
  9. package/package/components/ResourcePicker.svelte +17 -8
  10. package/package/components/SchemaForm.svelte +3 -3
  11. package/package/components/ScriptEditor.svelte +0 -5
  12. package/package/components/SearchItems.svelte +1 -1
  13. package/package/components/apps/editor/inlineScriptsPanel/InlineScriptsPanel.svelte +65 -48
  14. package/package/components/common/toggleButton-v2/ToggleButtonGroup.svelte +2 -1
  15. package/package/components/common/toggleButton-v2/ToggleButtonGroup.svelte.d.ts +1 -0
  16. package/package/components/copilot/chat/AIChat.svelte +0 -9
  17. package/package/components/copilot/chat/AIChat.svelte.d.ts +0 -2
  18. package/package/components/copilot/chat/AIChatDisplay.svelte +36 -26
  19. package/package/components/copilot/chat/AIChatInlineWidget.svelte +209 -0
  20. package/package/components/copilot/chat/AIChatInlineWidget.svelte.d.ts +15 -0
  21. package/package/components/copilot/chat/AIChatInput.svelte +50 -38
  22. package/package/components/copilot/chat/AIChatInput.svelte.d.ts +7 -0
  23. package/package/components/copilot/chat/AIChatManager.svelte.js +96 -9
  24. package/package/components/copilot/chat/AIChatMessage.svelte +15 -7
  25. package/package/components/copilot/chat/ChatQuickActions.svelte +1 -1
  26. package/package/components/copilot/chat/ContextElementBadge.svelte +1 -1
  27. package/package/components/copilot/chat/ContextTextarea.svelte +16 -7
  28. package/package/components/copilot/chat/ContextTextarea.svelte.d.ts +2 -1
  29. package/package/components/copilot/chat/GlobalReviewButtons.svelte +10 -8
  30. package/package/components/copilot/chat/GlobalReviewButtons.svelte.d.ts +5 -19
  31. package/package/components/copilot/chat/monaco-adapter.js +2 -2
  32. package/package/components/copilot/chat/script/core.d.ts +1 -0
  33. package/package/components/copilot/chat/script/core.js +84 -0
  34. package/package/components/flows/content/FlowModuleComponent.svelte +0 -8
  35. package/package/components/flows/header/FlowYamlEditor.svelte +8 -4
  36. package/package/components/flows/header/FlowYamlEditor.svelte.d.ts +4 -18
  37. package/package/components/home/ItemsList.svelte +3 -3
  38. package/package/components/home/ListFilters.svelte +5 -9
  39. package/package/components/home/ListFilters.svelte.d.ts +5 -20
  40. package/package/components/triggers/schedules/ScheduleEditorInner.svelte +7 -1
  41. package/package/components/workspaceSettings/AISettings.svelte +1 -0
  42. package/package/gen/schemas.gen.d.ts +0 -3
  43. package/package/gen/schemas.gen.js +0 -3
  44. package/package/gen/types.gen.d.ts +0 -1
  45. package/package.json +1 -1
  46. package/package/components/copilot/chat/AIChatManager.svelte.d.ts +0 -84
@@ -34,6 +34,14 @@ function submitSuggestion(suggestion) {
34
34
  export function focusInput() {
35
35
  aiChatInput?.focusInput();
36
36
  }
37
+ $effect(() => {
38
+ if (aiChatInput) {
39
+ aiChatManager.setAiChatInput(aiChatInput);
40
+ }
41
+ return () => {
42
+ aiChatManager.setAiChatInput(null);
43
+ };
44
+ });
37
45
  </script>
38
46
 
39
47
  <div class="flex flex-col h-full">
@@ -182,32 +190,34 @@ export function focusInput() {
182
190
  </Button>
183
191
  </div>
184
192
  {/if}
185
- <AIChatInput
186
- bind:this={aiChatInput}
187
- bind:selectedContext
188
- {availableContext}
189
- {disabled}
190
- isFirstMessage={messages.length === 0}
191
- placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
192
- />
193
- <div
194
- class={`flex flex-row ${
195
- aiChatManager.mode === 'script' && hasDiff ? 'justify-between' : 'justify-end'
196
- } items-center px-0.5`}
197
- >
198
- {#if aiChatManager.mode === 'script' && hasDiff}
199
- <ChatQuickActions {askAi} {diffMode} />
200
- {/if}
201
- {#if disabled}
202
- <div class="text-tertiary text-xs my-2 px-2">
203
- <Markdown md={disabledMessage} />
204
- </div>
205
- {:else}
206
- <div class="flex flex-row gap-2 min-w-0">
207
- <ChatMode />
208
- <ProviderModelSelector />
209
- </div>
210
- {/if}
193
+ <div class="px-2">
194
+ <AIChatInput
195
+ bind:this={aiChatInput}
196
+ bind:selectedContext
197
+ {availableContext}
198
+ {disabled}
199
+ isFirstMessage={messages.length === 0}
200
+ placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
201
+ />
202
+ <div
203
+ class={`flex flex-row ${
204
+ aiChatManager.mode === 'script' && hasDiff ? 'justify-between' : 'justify-end'
205
+ } items-center`}
206
+ >
207
+ {#if aiChatManager.mode === 'script' && hasDiff}
208
+ <ChatQuickActions {askAi} {diffMode} />
209
+ {/if}
210
+ {#if disabled}
211
+ <div class="text-tertiary text-xs my-2 px-2">
212
+ <Markdown md={disabledMessage} />
213
+ </div>
214
+ {:else}
215
+ <div class="flex flex-row gap-2 min-w-0">
216
+ <ChatMode />
217
+ <ProviderModelSelector />
218
+ </div>
219
+ {/if}
220
+ </div>
211
221
  </div>
212
222
  {#if (aiChatManager.mode === AIMode.NAVIGATOR || aiChatManager.mode === AIMode.ASK) && suggestions.length > 0 && messages.filter((m) => m.role === 'user').length === 0 && !disabled}
213
223
  <div class="px-2 mt-4">
@@ -0,0 +1,209 @@
1
+ <script lang="ts">import * as monaco from 'monaco-editor';
2
+ import AIChatInput from './AIChatInput.svelte';
3
+ import { aiChatManager, AIMode } from './AIChatManager.svelte';
4
+ import LoadingIcon from '../../apps/svelte-select/lib/LoadingIcon.svelte';
5
+ import { sendUserToast } from '../../../toast';
6
+ import { onDestroy } from 'svelte';
7
+ import { getModifierKey } from '../../../utils';
8
+ let { editor, selection, selectedCode, show = $bindable(false), editorHandler } = $props();
9
+ let widget = $state(null);
10
+ let widgetElement = $state(null);
11
+ let aiChatInput = $state(null);
12
+ let processing = $state(false);
13
+ let marginToAdd = $state(0);
14
+ class AIChatWidget {
15
+ domNode;
16
+ position;
17
+ editor;
18
+ originalPadding = 0;
19
+ editorHandler;
20
+ constructor(lineNumber, domNode, editor, editorHandler) {
21
+ this.domNode = domNode;
22
+ this.position = { lineNumber, column: 0 };
23
+ this.editor = editor;
24
+ this.editorHandler = editorHandler;
25
+ this.ensureSpaceAbove();
26
+ this.editor.onDidScrollChange((e) => {
27
+ this.domNode.style.visibility = 'inherit';
28
+ });
29
+ }
30
+ ensureSpaceAbove() {
31
+ const widgetHeight = 100;
32
+ const additionalPadding = 15;
33
+ const linesHeight = 20 * this.position.lineNumber;
34
+ if (linesHeight < widgetHeight + additionalPadding) {
35
+ // Add top margin to editor to make space
36
+ const editorDom = this.editor.getDomNode();
37
+ if (editorDom) {
38
+ const neededPadding = widgetHeight + additionalPadding - linesHeight;
39
+ this.originalPadding = this.editor.getOption(monaco.editor.EditorOption.padding)?.top || 0;
40
+ this.editor.updateOptions({
41
+ padding: {
42
+ top: neededPadding
43
+ }
44
+ });
45
+ // Trigger layout update
46
+ this.editor.layout();
47
+ }
48
+ }
49
+ }
50
+ restoreEditorSpacing() {
51
+ if (this.position.lineNumber < 10) {
52
+ const editorDom = this.editor.getDomNode();
53
+ if (editorDom) {
54
+ this.editor.updateOptions({
55
+ padding: {
56
+ top: this.originalPadding
57
+ }
58
+ });
59
+ this.editor.layout();
60
+ }
61
+ }
62
+ }
63
+ getId() {
64
+ return 'ai-inline-chat-widget';
65
+ }
66
+ getDomNode() {
67
+ return this.domNode;
68
+ }
69
+ getAddedLines() {
70
+ if (!this.editorHandler || !this.editorHandler.groupChanges) {
71
+ return 0;
72
+ }
73
+ let totalAddedLines = 0;
74
+ let totalRemovedLines = 0;
75
+ for (const group of this.editorHandler.groupChanges) {
76
+ for (const change of group.changes) {
77
+ if (change.type === 'added_block') {
78
+ // Count newlines in the added content
79
+ const lines = change.value.split('\n').length - 1;
80
+ totalAddedLines += Math.max(1, lines);
81
+ }
82
+ else if (change.type === 'deleted') {
83
+ const lines = change.range.endLine - change.range.startLine + 1;
84
+ totalRemovedLines += Math.max(1, lines);
85
+ }
86
+ }
87
+ }
88
+ return totalAddedLines - totalRemovedLines;
89
+ }
90
+ getPosition() {
91
+ return {
92
+ position: this.position,
93
+ preference: [monaco.editor.ContentWidgetPositionPreference.ABOVE]
94
+ };
95
+ }
96
+ dispose() {
97
+ this.restoreEditorSpacing();
98
+ }
99
+ }
100
+ // Cleanup function to safely remove widget and cancel requests
101
+ function cleanupWidget() {
102
+ aiChatManager.cancel();
103
+ if (widget) {
104
+ try {
105
+ widget.dispose();
106
+ editor.removeContentWidget(widget);
107
+ }
108
+ catch (error) {
109
+ console.warn('Failed to remove content widget:', error);
110
+ }
111
+ widget = null;
112
+ }
113
+ }
114
+ // Create/remove widget based on show state
115
+ $effect(() => {
116
+ if (show && !widget && widgetElement && selection) {
117
+ if (aiChatManager.mode !== AIMode.SCRIPT) {
118
+ aiChatManager.changeMode(AIMode.SCRIPT);
119
+ }
120
+ const startLine = selection.startLineNumber;
121
+ widget = new AIChatWidget(startLine, widgetElement, editor, editorHandler);
122
+ editor.addContentWidget(widget);
123
+ if (aiChatInput) {
124
+ aiChatInput.focusInput();
125
+ }
126
+ }
127
+ else if (!show && widget) {
128
+ cleanupWidget();
129
+ }
130
+ });
131
+ onDestroy(() => {
132
+ cleanupWidget();
133
+ });
134
+ export function focusInput() {
135
+ setTimeout(() => {
136
+ aiChatInput?.focusInput();
137
+ }, 130);
138
+ }
139
+ // Reactive effect to update margin when review state changes
140
+ $effect(() => {
141
+ const isInReviewMode = aiChatManager.pendingNewCode !== undefined;
142
+ if (isInReviewMode && widget) {
143
+ const addedLines = widget.getAddedLines();
144
+ if (widgetElement) {
145
+ marginToAdd = addedLines * 25;
146
+ widgetElement.style.marginTop = `-${marginToAdd}px`;
147
+ }
148
+ }
149
+ else {
150
+ marginToAdd = 0;
151
+ }
152
+ });
153
+ </script>
154
+
155
+ {#snippet bottomRightSnippet()}
156
+ {#if processing}
157
+ <LoadingIcon />
158
+ {:else if aiChatManager.pendingNewCode}
159
+ <span class="text-xs text-tertiary pr-1">
160
+ {getModifierKey()}↓ to apply
161
+ </span>
162
+ {:else}
163
+ <div></div>
164
+ {/if}
165
+ {/snippet}
166
+
167
+ {#if show}
168
+ <div bind:this={widgetElement} class="w-[300px] -mt-2">
169
+ <AIChatInput
170
+ bind:this={aiChatInput}
171
+ availableContext={aiChatManager.contextManager.getAvailableContext()}
172
+ selectedContext={aiChatManager.contextManager.getSelectedContext()}
173
+ onClickOutside={() => {
174
+ show = false
175
+ }}
176
+ onSendRequest={async (instructions) => {
177
+ if (!selection || processing) {
178
+ return
179
+ }
180
+
181
+ processing = true
182
+
183
+ try {
184
+ const reply = await aiChatManager.sendInlineRequest(instructions, selectedCode, selection)
185
+ if (reply) {
186
+ aiChatManager.scriptEditorApplyCode?.(reply)
187
+ }
188
+ } catch (error) {
189
+ console.error('Inline AI request failed:', error)
190
+ if (error instanceof Error) {
191
+ sendUserToast('AI request failed: ' + error.message, true)
192
+ } else {
193
+ sendUserToast('AI request failed: Unknown error', true)
194
+ }
195
+ } finally {
196
+ processing = false
197
+ }
198
+
199
+ focusInput()
200
+ }}
201
+ showContext={false}
202
+ className="-ml-2"
203
+ bottomRightSnippet={processing || aiChatManager.pendingNewCode
204
+ ? bottomRightSnippet
205
+ : undefined}
206
+ disabled={processing}
207
+ />
208
+ </div>
209
+ {/if}
@@ -0,0 +1,15 @@
1
+ import * as monaco from 'monaco-editor';
2
+ import type { Selection } from 'monaco-editor';
3
+ import type { AIChatEditorHandler } from './monaco-adapter';
4
+ interface Props {
5
+ editor: monaco.editor.IStandaloneCodeEditor;
6
+ selection: Selection | null;
7
+ selectedCode: string;
8
+ show: boolean;
9
+ editorHandler: AIChatEditorHandler;
10
+ }
11
+ declare const AiChatInlineWidget: import("svelte").Component<Props, {
12
+ focusInput: () => void;
13
+ }, "show">;
14
+ type AiChatInlineWidget = ReturnType<typeof AiChatInlineWidget>;
15
+ export default AiChatInlineWidget;
@@ -4,7 +4,8 @@ import ContextElementBadge from './ContextElementBadge.svelte';
4
4
  import ContextTextarea from './ContextTextarea.svelte';
5
5
  import autosize from '../../../autosize';
6
6
  import { aiChatManager } from './AIChatManager.svelte';
7
- let { availableContext, selectedContext = $bindable([]), disabled = false, isFirstMessage = false, placeholder = 'Ask anything', initialInstructions = '', editingMessageIndex = null, onEditEnd = () => { } } = $props();
7
+ import { twMerge } from 'tailwind-merge';
8
+ let { availableContext, selectedContext = $bindable([]), disabled = false, isFirstMessage = false, placeholder = 'Ask anything', initialInstructions = '', editingMessageIndex = null, onEditEnd = () => { }, className = '', onClickOutside = () => { }, onSendRequest = undefined, showContext = true, bottomRightSnippet, onKeyDown = undefined } = $props();
8
9
  let contextTextareaComponent = $state();
9
10
  let instructionsTextareaComponent = $state();
10
11
  let instructions = $state(initialInstructions);
@@ -18,8 +19,8 @@ export function focusInput() {
18
19
  }
19
20
  function clickOutside(node) {
20
21
  function handleClick(event) {
21
- if (node && !node.contains(event.target) && editingMessageIndex !== null) {
22
- onEditEnd();
22
+ if (node && !node.contains(event.target)) {
23
+ onClickOutside();
23
24
  }
24
25
  }
25
26
  document.addEventListener('click', handleClick, true);
@@ -57,39 +58,41 @@ $effect(() => {
57
58
  });
58
59
  </script>
59
60
 
60
- <div use:clickOutside>
61
+ <div use:clickOutside class="relative">
61
62
  {#if aiChatManager.mode === 'script'}
62
- <div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 px-2 no-scrollbar">
63
- <Popover>
64
- <svelte:fragment slot="trigger">
65
- <div
66
- class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover"
67
- >@</div
68
- >
69
- </svelte:fragment>
70
- <svelte:fragment slot="content" let:close>
71
- <AvailableContextList
72
- {availableContext}
73
- {selectedContext}
74
- onSelect={(element) => {
75
- addContextToSelection(element)
76
- close()
63
+ {#if showContext}
64
+ <div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 no-scrollbar">
65
+ <Popover>
66
+ <svelte:fragment slot="trigger">
67
+ <div
68
+ class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover bg-surface"
69
+ >@</div
70
+ >
71
+ </svelte:fragment>
72
+ <svelte:fragment slot="content" let:close>
73
+ <AvailableContextList
74
+ {availableContext}
75
+ {selectedContext}
76
+ onSelect={(element) => {
77
+ addContextToSelection(element)
78
+ close()
79
+ }}
80
+ />
81
+ </svelte:fragment>
82
+ </Popover>
83
+ {#each selectedContext as element}
84
+ <ContextElementBadge
85
+ contextElement={element}
86
+ deletable
87
+ on:delete={() => {
88
+ selectedContext = selectedContext?.filter(
89
+ (c) => c.type !== element.type || c.title !== element.title
90
+ )
77
91
  }}
78
92
  />
79
- </svelte:fragment>
80
- </Popover>
81
- {#each selectedContext as element}
82
- <ContextElementBadge
83
- contextElement={element}
84
- deletable
85
- on:delete={() => {
86
- selectedContext = selectedContext?.filter(
87
- (c) => c.type !== element.type || c.title !== element.title
88
- )
89
- }}
90
- />
91
- {/each}
92
- </div>
93
+ {/each}
94
+ </div>
95
+ {/if}
93
96
  <ContextTextarea
94
97
  bind:this={contextTextareaComponent}
95
98
  bind:value={instructions}
@@ -99,23 +102,27 @@ $effect(() => {
99
102
  {placeholder}
100
103
  onAddContext={(contextElement) => addContextToSelection(contextElement)}
101
104
  onSendRequest={() => {
102
- sendRequest()
105
+ if (disabled) {
106
+ return
107
+ }
108
+ onSendRequest ? onSendRequest(instructions) : sendRequest()
103
109
  }}
104
110
  {disabled}
105
- onEscape={onEditEnd}
111
+ {onKeyDown}
106
112
  />
107
113
  {:else}
108
- <div class="relative w-full px-2 scroll-pb-2 pt-2">
114
+ <div class={twMerge('relative w-full scroll-pb-2 pt-2', className)}>
109
115
  <textarea
110
116
  bind:this={instructionsTextareaComponent}
111
117
  bind:value={instructions}
112
118
  use:autosize
113
119
  onkeydown={(e) => {
120
+ if (onKeyDown) {
121
+ onKeyDown(e)
122
+ }
114
123
  if (e.key === 'Enter' && !e.shiftKey) {
115
124
  e.preventDefault()
116
125
  sendRequest()
117
- } else if (e.key === 'Escape') {
118
- onEditEnd()
119
126
  }
120
127
  }}
121
128
  rows={3}
@@ -125,4 +132,9 @@ $effect(() => {
125
132
  ></textarea>
126
133
  </div>
127
134
  {/if}
135
+ {#if bottomRightSnippet}
136
+ <div class="absolute bottom-2 right-2">
137
+ {@render bottomRightSnippet()}
138
+ </div>
139
+ {/if}
128
140
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { ContextElement } from './context';
2
+ import type { Snippet } from 'svelte';
2
3
  interface Props {
3
4
  availableContext: ContextElement[];
4
5
  selectedContext: ContextElement[];
@@ -8,6 +9,12 @@ interface Props {
8
9
  initialInstructions?: string;
9
10
  editingMessageIndex?: number | null;
10
11
  onEditEnd?: () => void;
12
+ className?: string;
13
+ onClickOutside?: () => void;
14
+ onSendRequest?: (instructions: string) => void;
15
+ showContext?: boolean;
16
+ bottomRightSnippet?: Snippet;
17
+ onKeyDown?: (e: KeyboardEvent) => void;
11
18
  }
12
19
  declare const AiChatInput: import("svelte").Component<Props, {
13
20
  focusInput: () => void;
@@ -2,7 +2,7 @@ import { flowTools, prepareFlowSystemMessage, prepareFlowUserMessage } from './f
2
2
  import ContextManager from './ContextManager.svelte';
3
3
  import HistoryManager from './HistoryManager.svelte';
4
4
  import { processToolCall } from './shared';
5
- import { prepareScriptSystemMessage, prepareScriptTools } from './script/core';
5
+ import { INLINE_CHAT_SYSTEM_PROMPT, prepareScriptSystemMessage, prepareScriptTools } from './script/core';
6
6
  import { navigatorTools, prepareNavigatorSystemMessage } from './navigator/core';
7
7
  import { loadApiTools } from './navigator/apiTools';
8
8
  import { prepareScriptUserMessage } from './script/core';
@@ -51,6 +51,7 @@ class AIChatManager {
51
51
  flowAiChatHelpers = $state(undefined);
52
52
  pendingNewCode = $state(undefined);
53
53
  apiTools = $state([]);
54
+ aiChatInput = $state(null);
54
55
  allowedModes = $derived({
55
56
  script: this.scriptEditorOptions !== undefined,
56
57
  flow: this.flowAiChatHelpers !== undefined,
@@ -58,12 +59,22 @@ class AIChatManager {
58
59
  ask: true
59
60
  });
60
61
  open = $derived(chatState.size > 0);
61
- async loadApiTools() {
62
- if (this.apiTools.length === 0) {
63
- this.apiTools = await loadApiTools();
64
- if (this.mode === AIMode.NAVIGATOR) {
65
- this.tools = [this.changeModeTool, ...navigatorTools, ...this.apiTools];
66
- }
62
+ constructor() {
63
+ loadApiTools()
64
+ .then((tools) => {
65
+ this.apiTools = tools;
66
+ })
67
+ .catch((err) => {
68
+ console.error('Error loading api tools', err);
69
+ this.apiTools = [];
70
+ });
71
+ }
72
+ setAiChatInput(aiChatInput) {
73
+ this.aiChatInput = aiChatInput;
74
+ }
75
+ focusInput() {
76
+ if (this.aiChatInput) {
77
+ this.aiChatInput.focusInput();
67
78
  }
68
79
  }
69
80
  updateMode(currentMode) {
@@ -199,11 +210,11 @@ class AIChatManager {
199
210
  lastUserMessage.error = true;
200
211
  }
201
212
  };
202
- chatRequest = async ({ messages, abortController, callbacks }) => {
213
+ chatRequest = async ({ messages, abortController, callbacks, systemMessage: systemMessageOverride }) => {
203
214
  try {
204
215
  let completion = null;
205
216
  while (true) {
206
- const systemMessage = this.systemMessage;
217
+ const systemMessage = systemMessageOverride ?? this.systemMessage;
207
218
  const tools = this.tools;
208
219
  const helpers = this.helpers;
209
220
  let pendingPrompt = this.pendingPrompt;
@@ -308,6 +319,81 @@ class AIChatManager {
308
319
  }
309
320
  }
310
321
  };
322
+ sendInlineRequest = async (instructions, selectedCode, selection) => {
323
+ // Validate inputs
324
+ if (!instructions.trim()) {
325
+ throw new Error('Instructions are required');
326
+ }
327
+ this.abortController = new AbortController();
328
+ const lang = this.scriptEditorOptions?.lang ?? 'bun';
329
+ const selectedContext = [...this.contextManager.getSelectedContext()];
330
+ const startLine = selection.startLineNumber;
331
+ const endLine = selection.endLineNumber;
332
+ selectedContext.push({
333
+ type: 'code_piece',
334
+ lang,
335
+ title: `L${startLine}-L${endLine}`,
336
+ startLine,
337
+ endLine,
338
+ content: selectedCode
339
+ });
340
+ const systemMessage = {
341
+ role: 'system',
342
+ content: INLINE_CHAT_SYSTEM_PROMPT
343
+ };
344
+ let reply = '';
345
+ try {
346
+ const userMessage = await prepareScriptUserMessage(instructions, lang, selectedContext, {
347
+ isPreprocessor: false
348
+ });
349
+ const messages = [userMessage];
350
+ const params = {
351
+ messages,
352
+ abortController: this.abortController,
353
+ callbacks: {
354
+ onNewToken: (token) => {
355
+ reply += token;
356
+ },
357
+ onMessageEnd: () => { },
358
+ setToolStatus: () => { }
359
+ },
360
+ systemMessage
361
+ };
362
+ await this.chatRequest({ ...params });
363
+ // Validate we received a response
364
+ if (!reply.trim()) {
365
+ throw new Error('AI response was empty');
366
+ }
367
+ // Try to extract new code from response
368
+ const newCodeMatch = reply.match(/<new_code>([\s\S]*?)<\/new_code>/i);
369
+ if (newCodeMatch && newCodeMatch[1]) {
370
+ const code = newCodeMatch[1].trim();
371
+ if (!code) {
372
+ throw new Error('AI response contained empty code block');
373
+ }
374
+ return code;
375
+ }
376
+ // Fallback: try to take everything after the last <new_code> tag
377
+ const lastNewCodeMatch = reply.match(/<new_code>([\s\S]*)/i);
378
+ if (lastNewCodeMatch && lastNewCodeMatch[1]) {
379
+ const code = lastNewCodeMatch[1].trim().replace(/```/g, '');
380
+ if (!code) {
381
+ throw new Error('AI response contained empty code block');
382
+ }
383
+ return code;
384
+ }
385
+ // If no code tags found, throw error with helpful message
386
+ throw new Error('AI response did not contain valid code. Please try rephrasing your request.');
387
+ }
388
+ catch (error) {
389
+ // if abort controller is aborted, don't throw an error
390
+ if (this.abortController?.signal.aborted) {
391
+ return;
392
+ }
393
+ console.error('Unexpected error in sendInlineRequest:', error);
394
+ throw new Error('An unexpected error occurred. Please try again.');
395
+ }
396
+ };
311
397
  sendRequest = async (options = {}) => {
312
398
  if (options.mode) {
313
399
  this.changeMode(options.mode);
@@ -450,6 +536,7 @@ class AIChatManager {
450
536
  }
451
537
  this.changeMode(AIMode.SCRIPT);
452
538
  this.contextManager?.addSelectedLinesToContext(lines, startLine, endLine);
539
+ this.focusInput();
453
540
  };
454
541
  saveAndClear = async () => {
455
542
  await this.historyManager.save(this.displayMessages, this.messages);
@@ -33,13 +33,21 @@ function editMessage() {
33
33
  </div>
34
34
  {/if}
35
35
  {#if message.role === 'user' && editingMessageIndex === messageIndex}
36
- <AIChatInput
37
- {availableContext}
38
- bind:selectedContext
39
- initialInstructions={message.content}
40
- {editingMessageIndex}
41
- onEditEnd={() => (editingMessageIndex = null)}
42
- />
36
+ <div class="px-2">
37
+ <AIChatInput
38
+ {availableContext}
39
+ bind:selectedContext
40
+ initialInstructions={message.content}
41
+ {editingMessageIndex}
42
+ onClickOutside={() => (editingMessageIndex = null)}
43
+ onKeyDown={(e) => {
44
+ if (e.key === 'Escape') {
45
+ editingMessageIndex = null
46
+ }
47
+ }}
48
+ onEditEnd={() => (editingMessageIndex = null)}
49
+ />
50
+ </div>
43
51
  {:else}
44
52
  <div
45
53
  class={twMerge(
@@ -7,7 +7,7 @@ let btnClasses = $derived(`!px-1 !py-0.5 !gap-1 ${diffMode
7
7
  : '!font-normal'}`);
8
8
  </script>
9
9
 
10
- <div class="flex flex-row items-center gap-2 px-2 py-1">
10
+ <div class="flex flex-row items-center gap-2 pr-1 py-1">
11
11
  <div class="flex flex-row items-center gap-1.5">
12
12
  {#if diffMode}
13
13
  <Button
@@ -19,7 +19,7 @@ const dispatch = createEventDispatcher();
19
19
  <svelte:fragment slot="trigger">
20
20
  <div
21
21
  class={twMerge(
22
- 'border rounded-md px-1 py-0.5 flex flex-row items-center gap-1 text-tertiary text-xs cursor-default hover:bg-surface-hover hover:cursor-pointer max-w-48'
22
+ 'border rounded-md px-1 py-0.5 flex flex-row items-center gap-1 text-tertiary text-xs cursor-default hover:bg-surface-hover hover:cursor-pointer max-w-48 bg-surface'
23
23
  )}
24
24
  on:mouseenter={() => (showDelete = true)}
25
25
  on:mouseleave={() => (showDelete = false)}