jp.keijiro.ai.assistant.extensions 1.1.2 → 1.2.0
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/.attestation.p7m +0 -0
- package/CHANGELOG.md +18 -0
- package/Editor/ConversationExtractorWindow.cs +474 -130
- package/Editor/ConversationExtractorWindow.uss +80 -0
- package/Editor/ConversationExtractorWindow.uss.meta +12 -0
- package/Editor/ConversationExtractorWindow.uxml +23 -0
- package/Editor/ConversationExtractorWindow.uxml.meta +10 -0
- package/Editor/ConversationRow.uxml +6 -0
- package/Editor/ConversationRow.uxml.meta +10 -0
- package/package.json +4 -4
- package/Editor/PackageSkillRegistrar.cs +0 -150
- package/Editor/PackageSkillRegistrar.cs.meta +0 -11
- /package/{Skills → AIAssistantSkills}/GameViewCapture/SKILL.md +0 -0
- /package/{Skills → AIAssistantSkills}/GameViewCapture/SKILL.md.meta +0 -0
- /package/{Skills → AIAssistantSkills}/GameViewCapture.meta +0 -0
- /package/{Skills.meta → AIAssistantSkills.meta} +0 -0
package/.attestation.p7m
CHANGED
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-06-05
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Reworked the Conversation Extractor to read conversations through the AI Assistant in-process API instead of parsing the relay log.
|
|
13
|
+
- Rebuilt the Conversation Extractor window with UI Toolkit, adding a resizable conversation list, per-conversation selection, a scrollable read-only transcript, and a toggle for including tool calls.
|
|
14
|
+
- Updated project and package metadata for AI Assistant 2.11.0-pre.1.
|
|
15
|
+
|
|
16
|
+
## [1.1.3] - 2026-05-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed package skill registration for AI Assistant 2.8.0-pre.1 by using the built-in package skill discovery path.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Updated project and package metadata for AI Assistant 2.8.0-pre.1.
|
|
25
|
+
|
|
8
26
|
## [1.1.2] - 2026-05-08
|
|
9
27
|
|
|
10
28
|
### Fixed
|
|
@@ -1,227 +1,571 @@
|
|
|
1
1
|
using System;
|
|
2
|
+
using System.Collections;
|
|
2
3
|
using System.Collections.Generic;
|
|
3
4
|
using System.IO;
|
|
5
|
+
using System.Linq;
|
|
6
|
+
using System.Reflection;
|
|
4
7
|
using System.Text;
|
|
5
|
-
using System.
|
|
8
|
+
using System.Threading;
|
|
9
|
+
using System.Threading.Tasks;
|
|
6
10
|
using UnityEditor;
|
|
11
|
+
using UnityEditor.UIElements;
|
|
7
12
|
using UnityEngine;
|
|
13
|
+
using UnityEngine.UIElements;
|
|
8
14
|
|
|
9
15
|
namespace AIAssistantExtensions {
|
|
10
16
|
|
|
17
|
+
// Lists the (Unity-hosted) conversations from the AI Assistant package's in-process
|
|
18
|
+
// API and extracts whichever one you pick. Everything in that API is internal and
|
|
19
|
+
// our assembly is not on its InternalsVisibleTo allowlist, so we reach it through
|
|
20
|
+
// reflection. The live IAssistantProvider only exists while an AI Assistant window
|
|
21
|
+
// is open, so that window must be open for this to work.
|
|
11
22
|
class ConversationExtractorWindow : EditorWindow
|
|
12
23
|
{
|
|
13
|
-
const string
|
|
24
|
+
const string AssetDir = "Packages/jp.keijiro.ai.assistant.extensions/Editor/";
|
|
25
|
+
const string UxmlPath = AssetDir + "ConversationExtractorWindow.uxml";
|
|
26
|
+
const string RowUxmlPath = AssetDir + "ConversationRow.uxml";
|
|
27
|
+
const string UssPath = AssetDir + "ConversationExtractorWindow.uss";
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
RegexOptions.Compiled);
|
|
29
|
+
const string AssistantWindowTypeName = "Unity.AI.Assistant.UI.Editor.Scripts.AssistantWindow";
|
|
30
|
+
const int EventTimeoutMs = 20000;
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
const BindingFlags InstanceMembers =
|
|
33
|
+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
34
|
+
|
|
35
|
+
struct ConversationItem
|
|
36
|
+
{
|
|
37
|
+
public object IdBoxed; // boxed AssistantConversationId, passed straight back to the API
|
|
38
|
+
public string Id; // its string value, used to match the load callback
|
|
39
|
+
public string Title;
|
|
40
|
+
public long Timestamp;
|
|
41
|
+
public bool Favorite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
readonly List<ConversationItem> _conversations = new();
|
|
45
|
+
|
|
46
|
+
object _loadedConversation; // last loaded conversation, for rebuilding without re-fetching
|
|
20
47
|
string _markdown;
|
|
21
|
-
string _status;
|
|
22
|
-
|
|
48
|
+
string _status; // right pane: extraction lifecycle only
|
|
49
|
+
string _listStatus; // left pane: empty-state message
|
|
50
|
+
bool _busy;
|
|
51
|
+
bool _extracting;
|
|
52
|
+
bool _includeToolCalls = true;
|
|
53
|
+
|
|
54
|
+
// UI elements (resolved in CreateGUI).
|
|
55
|
+
VisualTreeAsset _rowTemplate;
|
|
56
|
+
ToolbarButton _refreshButton;
|
|
57
|
+
Toggle _toolCallsToggle;
|
|
58
|
+
ToolbarButton _copyButton;
|
|
59
|
+
ToolbarButton _saveButton;
|
|
60
|
+
Label _listHeader;
|
|
61
|
+
Label _emptyLabel;
|
|
62
|
+
ListView _listView;
|
|
63
|
+
TextField _preview;
|
|
64
|
+
|
|
65
|
+
// Event plumbing shared with the reflection callbacks.
|
|
66
|
+
Delegate _refreshedHandler;
|
|
67
|
+
Delegate _loadedHandler;
|
|
68
|
+
TaskCompletionSource<object> _refreshTcs;
|
|
69
|
+
TaskCompletionSource<object> _loadTcs;
|
|
70
|
+
string _pendingLoadId;
|
|
23
71
|
|
|
24
72
|
[MenuItem("Window/AI/Conversation Extractor")]
|
|
25
73
|
static void Open()
|
|
26
74
|
{
|
|
27
75
|
var window = GetWindow<ConversationExtractorWindow>();
|
|
28
76
|
window.titleContent = new GUIContent("Conversation Extractor");
|
|
29
|
-
window.minSize = new Vector2(
|
|
30
|
-
window.Extract();
|
|
77
|
+
window.minSize = new Vector2(640, 360);
|
|
31
78
|
}
|
|
32
79
|
|
|
33
|
-
|
|
80
|
+
// --- UI construction ----------------------------------------------------
|
|
81
|
+
|
|
82
|
+
void CreateGUI()
|
|
34
83
|
{
|
|
35
|
-
|
|
84
|
+
var tree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
|
85
|
+
if (tree == null)
|
|
36
86
|
{
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
rootVisualElement.Add(new Label($"Layout asset not found: {UxmlPath}"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
39
90
|
|
|
40
|
-
|
|
91
|
+
tree.CloneTree(rootVisualElement);
|
|
41
92
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
|
94
|
+
if (styleSheet != null)
|
|
95
|
+
rootVisualElement.styleSheets.Add(styleSheet);
|
|
96
|
+
|
|
97
|
+
_rowTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(RowUxmlPath);
|
|
98
|
+
|
|
99
|
+
_refreshButton = rootVisualElement.Q<ToolbarButton>("refresh-button");
|
|
100
|
+
_toolCallsToggle = rootVisualElement.Q<Toggle>("toolcalls-toggle");
|
|
101
|
+
_copyButton = rootVisualElement.Q<ToolbarButton>("copy-button");
|
|
102
|
+
_saveButton = rootVisualElement.Q<ToolbarButton>("save-button");
|
|
103
|
+
_listHeader = rootVisualElement.Q<Label>("list-header");
|
|
104
|
+
_emptyLabel = rootVisualElement.Q<Label>("empty-label");
|
|
105
|
+
_listView = rootVisualElement.Q<ListView>("conversation-list");
|
|
106
|
+
_preview = rootVisualElement.Q<TextField>("preview");
|
|
107
|
+
|
|
108
|
+
_refreshButton.clicked += () => { if (!_busy) _ = RefreshList(); };
|
|
109
|
+
_copyButton.clicked += CopyToClipboard;
|
|
110
|
+
_saveButton.clicked += SaveMarkdown;
|
|
111
|
+
|
|
112
|
+
_toolCallsToggle.SetValueWithoutNotify(_includeToolCalls);
|
|
113
|
+
_toolCallsToggle.RegisterValueChangedCallback(evt => OnToolCallsChanged(evt.newValue));
|
|
114
|
+
|
|
115
|
+
_listView.itemsSource = _conversations;
|
|
116
|
+
_listView.makeItem = MakeRow;
|
|
117
|
+
_listView.bindItem = BindRow;
|
|
118
|
+
_listView.selectionChanged += OnSelectionChanged;
|
|
119
|
+
|
|
120
|
+
// Show the initial hint; RefreshList drives the toolbar and list state.
|
|
121
|
+
UpdatePreview();
|
|
122
|
+
_ = RefreshList();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
VisualElement MakeRow()
|
|
126
|
+
{
|
|
127
|
+
if (_rowTemplate != null)
|
|
128
|
+
return _rowTemplate.Instantiate();
|
|
129
|
+
|
|
130
|
+
var container = new VisualElement();
|
|
131
|
+
container.Add(new Label { name = "row-title" });
|
|
132
|
+
container.Add(new Label { name = "row-date" });
|
|
133
|
+
return container;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void BindRow(VisualElement element, int index)
|
|
137
|
+
{
|
|
138
|
+
var item = _conversations[index];
|
|
139
|
+
|
|
140
|
+
var title = element.Q<Label>("row-title");
|
|
141
|
+
// Titles can contain embedded newlines; collapse them so the row stays
|
|
142
|
+
// single-line (white-space:nowrap doesn't strip explicit line breaks).
|
|
143
|
+
title.text = (item.Favorite ? "★ " : "") + SingleLine(DisplayTitle(item.Title));
|
|
144
|
+
|
|
145
|
+
var date = element.Q<Label>("row-date");
|
|
146
|
+
var formatted = FormatDate(item.Timestamp);
|
|
147
|
+
date.text = formatted;
|
|
148
|
+
date.style.display = string.IsNullOrEmpty(formatted) ? DisplayStyle.None : DisplayStyle.Flex;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void OnSelectionChanged(IEnumerable<object> selection)
|
|
152
|
+
{
|
|
153
|
+
if (_busy) return;
|
|
154
|
+
|
|
155
|
+
var index = _listView.selectedIndex;
|
|
156
|
+
if (index >= 0 && index < _conversations.Count)
|
|
157
|
+
_ = ExtractConversation(_conversations[index]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
void OnToolCallsChanged(bool include)
|
|
161
|
+
{
|
|
162
|
+
_includeToolCalls = include;
|
|
163
|
+
|
|
164
|
+
// Rebuild from the already-loaded conversation; no need to re-fetch.
|
|
165
|
+
if (_busy || _loadedConversation == null) return;
|
|
166
|
+
_markdown = BuildConversationMarkdown(_loadedConversation, _includeToolCalls);
|
|
167
|
+
UpdateToolbar();
|
|
168
|
+
UpdatePreview();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- State -> UI --------------------------------------------------------
|
|
49
172
|
|
|
50
|
-
|
|
51
|
-
|
|
173
|
+
void UpdateToolbar()
|
|
174
|
+
{
|
|
175
|
+
_refreshButton?.SetEnabled(!_busy);
|
|
176
|
+
|
|
177
|
+
var hasMarkdown = !_busy && !string.IsNullOrEmpty(_markdown);
|
|
178
|
+
_copyButton?.SetEnabled(hasMarkdown);
|
|
179
|
+
_saveButton?.SetEnabled(hasMarkdown);
|
|
180
|
+
}
|
|
52
181
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
182
|
+
void UpdateList()
|
|
183
|
+
{
|
|
184
|
+
if (_listView == null) return;
|
|
185
|
+
|
|
186
|
+
_listHeader.text = $"Conversations ({_conversations.Count})";
|
|
187
|
+
_listView.Rebuild();
|
|
188
|
+
|
|
189
|
+
var empty = _conversations.Count == 0;
|
|
190
|
+
_listView.style.display = empty ? DisplayStyle.None : DisplayStyle.Flex;
|
|
191
|
+
_emptyLabel.style.display = empty ? DisplayStyle.Flex : DisplayStyle.None;
|
|
192
|
+
if (empty)
|
|
193
|
+
_emptyLabel.text = _busy
|
|
194
|
+
? "Loading..."
|
|
195
|
+
: (string.IsNullOrEmpty(_listStatus) ? "No conversations." : _listStatus);
|
|
56
196
|
}
|
|
57
197
|
|
|
58
|
-
void
|
|
198
|
+
void UpdatePreview()
|
|
59
199
|
{
|
|
60
|
-
|
|
61
|
-
|
|
200
|
+
if (_preview == null) return;
|
|
201
|
+
|
|
202
|
+
// While extracting show the status text; otherwise the extracted markdown,
|
|
203
|
+
// or a hint when there is none.
|
|
204
|
+
string content;
|
|
205
|
+
if (_extracting)
|
|
206
|
+
content = _status;
|
|
207
|
+
else if (!string.IsNullOrEmpty(_markdown))
|
|
208
|
+
content = _markdown;
|
|
209
|
+
else
|
|
210
|
+
content = string.IsNullOrEmpty(_status) ? "Select a conversation to extract." : _status;
|
|
211
|
+
|
|
212
|
+
_preview.SetValueWithoutNotify(content ?? "");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Operations ---------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
async Task RefreshList()
|
|
218
|
+
{
|
|
219
|
+
object provider = null;
|
|
220
|
+
_busy = true;
|
|
221
|
+
_listStatus = null;
|
|
222
|
+
UpdateToolbar();
|
|
223
|
+
UpdateList();
|
|
224
|
+
|
|
225
|
+
try
|
|
62
226
|
{
|
|
227
|
+
provider = GetLiveProvider(out var error);
|
|
228
|
+
if (provider == null) { _listStatus = error; return; }
|
|
229
|
+
|
|
230
|
+
Subscribe(provider);
|
|
231
|
+
|
|
232
|
+
_conversations.Clear();
|
|
233
|
+
_conversations.AddRange((await RefreshConversations(provider))
|
|
234
|
+
.OrderByDescending(c => c.Timestamp));
|
|
235
|
+
_loadedConversation = null;
|
|
63
236
|
_markdown = "";
|
|
64
|
-
_status =
|
|
65
|
-
|
|
237
|
+
_status = null;
|
|
238
|
+
_listView?.SetSelectionWithoutNotify(Array.Empty<int>());
|
|
239
|
+
}
|
|
240
|
+
catch (Exception e)
|
|
241
|
+
{
|
|
242
|
+
_listStatus = $"Failed to refresh: {e.Message}";
|
|
243
|
+
Debug.LogException(e);
|
|
244
|
+
}
|
|
245
|
+
finally
|
|
246
|
+
{
|
|
247
|
+
Unsubscribe(provider);
|
|
248
|
+
_busy = false;
|
|
249
|
+
UpdateToolbar();
|
|
250
|
+
UpdateList();
|
|
251
|
+
UpdatePreview();
|
|
66
252
|
}
|
|
253
|
+
}
|
|
67
254
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
255
|
+
async Task ExtractConversation(ConversationItem item)
|
|
256
|
+
{
|
|
257
|
+
object provider = null;
|
|
258
|
+
_busy = true;
|
|
259
|
+
_extracting = true;
|
|
260
|
+
_status = $"Extracting \"{DisplayTitle(item.Title)}\"...";
|
|
261
|
+
UpdateToolbar();
|
|
262
|
+
UpdatePreview();
|
|
263
|
+
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
provider = GetLiveProvider(out var error);
|
|
267
|
+
if (provider == null) { _markdown = ""; _status = error; return; }
|
|
268
|
+
|
|
269
|
+
Subscribe(provider);
|
|
270
|
+
|
|
271
|
+
var conversation = await LoadConversation(provider, item);
|
|
272
|
+
_loadedConversation = conversation;
|
|
273
|
+
_markdown = BuildConversationMarkdown(conversation, _includeToolCalls);
|
|
274
|
+
_status = null;
|
|
275
|
+
}
|
|
276
|
+
catch (Exception e)
|
|
277
|
+
{
|
|
278
|
+
_loadedConversation = null;
|
|
279
|
+
_markdown = "";
|
|
280
|
+
_status = $"Extraction failed: {e.Message}";
|
|
281
|
+
Debug.LogException(e);
|
|
282
|
+
}
|
|
283
|
+
finally
|
|
284
|
+
{
|
|
285
|
+
Unsubscribe(provider);
|
|
286
|
+
_busy = false;
|
|
287
|
+
_extracting = false;
|
|
288
|
+
UpdateToolbar();
|
|
289
|
+
UpdatePreview();
|
|
290
|
+
}
|
|
71
291
|
}
|
|
72
292
|
|
|
73
293
|
void CopyToClipboard()
|
|
74
294
|
{
|
|
75
295
|
EditorGUIUtility.systemCopyBuffer = _markdown ?? "";
|
|
76
|
-
|
|
296
|
+
ShowNotification(new GUIContent("Copied to clipboard"));
|
|
77
297
|
}
|
|
78
298
|
|
|
79
299
|
void SaveMarkdown()
|
|
80
300
|
{
|
|
81
|
-
var projectRoot =
|
|
82
|
-
var
|
|
83
|
-
var
|
|
301
|
+
var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? Directory.GetCurrentDirectory();
|
|
302
|
+
var index = _listView?.selectedIndex ?? -1;
|
|
303
|
+
var name = index >= 0 && index < _conversations.Count
|
|
304
|
+
? SanitizeFileName(_conversations[index].Title)
|
|
305
|
+
: "conversation";
|
|
306
|
+
var defaultPath = Path.Combine(projectRoot, "Logs", $"{name}.md");
|
|
307
|
+
var path = EditorUtility.SaveFilePanel("Save extracted conversation", Path.GetDirectoryName(defaultPath), Path.GetFileName(defaultPath), "md");
|
|
84
308
|
if (string.IsNullOrEmpty(path)) return;
|
|
85
309
|
|
|
86
310
|
File.WriteAllText(path, _markdown ?? "", Encoding.UTF8);
|
|
87
|
-
|
|
311
|
+
ShowNotification(new GUIContent($"Saved to {Path.GetFileName(path)}"));
|
|
88
312
|
}
|
|
89
313
|
|
|
90
|
-
|
|
314
|
+
// --- Live provider acquisition -----------------------------------------
|
|
315
|
+
|
|
316
|
+
static object GetLiveProvider(out string error)
|
|
91
317
|
{
|
|
92
|
-
|
|
93
|
-
ConversationPair currentPair = null;
|
|
94
|
-
string activeResponseId = null;
|
|
318
|
+
error = null;
|
|
95
319
|
|
|
96
|
-
var
|
|
97
|
-
|
|
320
|
+
var windowType = FindType(AssistantWindowTypeName);
|
|
321
|
+
if (windowType == null)
|
|
98
322
|
{
|
|
99
|
-
|
|
100
|
-
|
|
323
|
+
error = "AI Assistant package not found. Is it installed?";
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
101
326
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
327
|
+
var window = windowType
|
|
328
|
+
.GetMethod("FindExistingWindow", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
|
329
|
+
?.Invoke(null, null);
|
|
330
|
+
if (window == null)
|
|
331
|
+
{
|
|
332
|
+
error = "Open the AI Assistant window first (Window > AI > Assistant), then retry.";
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
106
335
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
336
|
+
var provider = windowType.GetProperty("AssistantInstance", InstanceMembers)?.GetValue(window);
|
|
337
|
+
if (provider == null)
|
|
338
|
+
error = "AI Assistant instance is not ready yet. Interact with the Assistant window, then retry.";
|
|
111
339
|
|
|
112
|
-
|
|
113
|
-
|
|
340
|
+
return provider;
|
|
341
|
+
}
|
|
114
342
|
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
activeResponseId = parsed.MessageId;
|
|
118
|
-
currentPair.ResponseMessageId = activeResponseId;
|
|
119
|
-
}
|
|
343
|
+
// --- Event-driven calls -------------------------------------------------
|
|
120
344
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
345
|
+
async Task<List<ConversationItem>> RefreshConversations(object provider)
|
|
346
|
+
{
|
|
347
|
+
_refreshTcs = new TaskCompletionSource<object>();
|
|
348
|
+
|
|
349
|
+
var method = provider.GetType().GetMethod(
|
|
350
|
+
"RefreshConversationsAsync", InstanceMembers,
|
|
351
|
+
null, new[] { typeof(CancellationToken), typeof(bool) }, null);
|
|
352
|
+
if (method == null)
|
|
353
|
+
throw new MissingMethodException("IAssistantProvider.RefreshConversationsAsync not found.");
|
|
354
|
+
|
|
355
|
+
await (Task)method.Invoke(provider, new object[] { CancellationToken.None, false });
|
|
356
|
+
var infos = await WithTimeout(_refreshTcs.Task);
|
|
126
357
|
|
|
127
|
-
|
|
358
|
+
// The event delivers IEnumerable<AssistantConversationInfo> (a boxed struct per item).
|
|
359
|
+
var result = new List<ConversationItem>();
|
|
360
|
+
foreach (var info in (IEnumerable)infos)
|
|
361
|
+
{
|
|
362
|
+
var idBoxed = GetMember(info, "Id");
|
|
363
|
+
result.Add(new ConversationItem
|
|
128
364
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
365
|
+
IdBoxed = idBoxed,
|
|
366
|
+
Id = GetMember(idBoxed, "Value") as string,
|
|
367
|
+
Title = GetMember(info, "Title") as string,
|
|
368
|
+
Timestamp = Convert.ToInt64(GetMember(info, "LastMessageTimestamp") ?? 0L),
|
|
369
|
+
Favorite = GetMember(info, "IsFavorite") is true
|
|
370
|
+
});
|
|
133
371
|
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async Task<object> LoadConversation(object provider, ConversationItem item)
|
|
376
|
+
{
|
|
377
|
+
_pendingLoadId = item.Id;
|
|
378
|
+
_loadTcs = new TaskCompletionSource<object>();
|
|
134
379
|
|
|
135
|
-
|
|
136
|
-
|
|
380
|
+
var method = provider.GetType().GetMethod("ConversationLoad", InstanceMembers);
|
|
381
|
+
if (method == null)
|
|
382
|
+
throw new MissingMethodException("IAssistantProvider.ConversationLoad not found.");
|
|
137
383
|
|
|
138
|
-
|
|
384
|
+
await (Task)method.Invoke(provider, new[] { item.IdBoxed, CancellationToken.None });
|
|
385
|
+
return await WithTimeout(_loadTcs.Task);
|
|
139
386
|
}
|
|
140
387
|
|
|
141
|
-
static
|
|
388
|
+
static async Task<object> WithTimeout(Task<object> task)
|
|
142
389
|
{
|
|
143
|
-
|
|
144
|
-
|
|
390
|
+
if (await Task.WhenAny(task, Task.Delay(EventTimeoutMs)) != task)
|
|
391
|
+
throw new TimeoutException("Timed out waiting for the AI Assistant API to respond.");
|
|
392
|
+
return await task;
|
|
393
|
+
}
|
|
145
394
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
395
|
+
void Subscribe(object provider)
|
|
396
|
+
{
|
|
397
|
+
var type = provider.GetType();
|
|
398
|
+
|
|
399
|
+
// Bind handlers taking `object` to Action<T> events via delegate contravariance.
|
|
400
|
+
var refreshedEvent = type.GetEvent("ConversationsRefreshed", InstanceMembers);
|
|
401
|
+
_refreshedHandler = Delegate.CreateDelegate(
|
|
402
|
+
refreshedEvent.EventHandlerType, this,
|
|
403
|
+
GetType().GetMethod(nameof(OnConversationsRefreshed), InstanceMembers));
|
|
404
|
+
refreshedEvent.AddEventHandler(provider, _refreshedHandler);
|
|
405
|
+
|
|
406
|
+
var loadedEvent = type.GetEvent("ConversationLoaded", InstanceMembers);
|
|
407
|
+
_loadedHandler = Delegate.CreateDelegate(
|
|
408
|
+
loadedEvent.EventHandlerType, this,
|
|
409
|
+
GetType().GetMethod(nameof(OnConversationLoaded), InstanceMembers));
|
|
410
|
+
loadedEvent.AddEventHandler(provider, _loadedHandler);
|
|
153
411
|
}
|
|
154
412
|
|
|
155
|
-
|
|
156
|
-
|
|
413
|
+
void Unsubscribe(object provider)
|
|
414
|
+
{
|
|
415
|
+
if (provider == null) return;
|
|
157
416
|
|
|
158
|
-
|
|
417
|
+
var type = provider.GetType();
|
|
418
|
+
if (_refreshedHandler != null)
|
|
419
|
+
type.GetEvent("ConversationsRefreshed", InstanceMembers)?.RemoveEventHandler(provider, _refreshedHandler);
|
|
420
|
+
if (_loadedHandler != null)
|
|
421
|
+
type.GetEvent("ConversationLoaded", InstanceMembers)?.RemoveEventHandler(provider, _loadedHandler);
|
|
422
|
+
|
|
423
|
+
_refreshedHandler = _loadedHandler = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Reflection callbacks. May arrive on a background thread; TaskCompletionSource
|
|
427
|
+
// marshals the awaiting continuation back to the captured (main) context.
|
|
428
|
+
void OnConversationsRefreshed(object infos) => _refreshTcs?.TrySetResult(infos);
|
|
429
|
+
|
|
430
|
+
void OnConversationLoaded(object conversation)
|
|
159
431
|
{
|
|
432
|
+
var id = GetMember(GetMember(conversation, "Id"), "Value") as string;
|
|
433
|
+
if (_loadTcs != null && id == _pendingLoadId)
|
|
434
|
+
_loadTcs.TrySetResult(conversation);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// --- Markdown building --------------------------------------------------
|
|
438
|
+
|
|
439
|
+
static string BuildConversationMarkdown(object conversation, bool includeToolCalls)
|
|
440
|
+
{
|
|
441
|
+
if (conversation == null) return "";
|
|
442
|
+
|
|
160
443
|
var builder = new StringBuilder();
|
|
444
|
+
var title = GetMember(conversation, "Title") as string;
|
|
445
|
+
builder.Append("# ").Append(DisplayTitle(title));
|
|
161
446
|
|
|
162
|
-
|
|
447
|
+
if (GetMember(conversation, "Messages") is IEnumerable messages)
|
|
163
448
|
{
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
builder.Append("\n\n");
|
|
449
|
+
foreach (var message in messages)
|
|
450
|
+
{
|
|
451
|
+
var text = ExtractMessageText(message, includeToolCalls);
|
|
452
|
+
if (string.IsNullOrWhiteSpace(text)) continue;
|
|
169
453
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
454
|
+
var role = GetMember(message, "Role") as string;
|
|
455
|
+
builder.Append("\n\n## ").Append(FormatRole(role)).Append("\n\n");
|
|
456
|
+
builder.Append(text.Trim());
|
|
457
|
+
}
|
|
174
458
|
}
|
|
175
459
|
|
|
176
460
|
return builder.ToString();
|
|
177
461
|
}
|
|
178
462
|
|
|
179
|
-
static string
|
|
180
|
-
|
|
463
|
+
static string ExtractMessageText(object message, bool includeToolCalls)
|
|
464
|
+
{
|
|
465
|
+
if (GetMember(message, "Blocks") is not IEnumerable blocks) return "";
|
|
181
466
|
|
|
182
|
-
|
|
183
|
-
|
|
467
|
+
var parts = new List<string>();
|
|
468
|
+
foreach (var block in blocks)
|
|
469
|
+
{
|
|
470
|
+
var text = BlockText(block, includeToolCalls);
|
|
471
|
+
if (!string.IsNullOrWhiteSpace(text)) parts.Add(text.Trim());
|
|
472
|
+
}
|
|
473
|
+
return string.Join("\n\n", parts);
|
|
474
|
+
}
|
|
184
475
|
|
|
185
|
-
|
|
186
|
-
|
|
476
|
+
// Pulls text out of each block. Text-bearing blocks return their content;
|
|
477
|
+
// FunctionCallBlock is rendered as the tool name plus its parameters (only when
|
|
478
|
+
// tool calls are included). The ACP tool-call / plan blocks carry structured
|
|
479
|
+
// data and are skipped.
|
|
480
|
+
static string BlockText(object block, bool includeToolCalls)
|
|
187
481
|
{
|
|
188
|
-
|
|
482
|
+
if (block == null) return null;
|
|
483
|
+
return block.GetType().Name switch
|
|
484
|
+
{
|
|
485
|
+
"PromptBlock" => GetMember(block, "Content") as string,
|
|
486
|
+
"AnswerBlock" => GetMember(block, "Content") as string,
|
|
487
|
+
"ThoughtBlock" => GetMember(block, "Content") as string,
|
|
488
|
+
"ErrorBlock" => GetMember(block, "Error") as string,
|
|
489
|
+
"InfoBlock" => GetMember(block, "Message") as string,
|
|
490
|
+
"FunctionCallBlock" => includeToolCalls ? FormatFunctionCall(GetMember(block, "Call")) : null,
|
|
491
|
+
_ => null
|
|
492
|
+
};
|
|
189
493
|
}
|
|
190
494
|
|
|
191
|
-
|
|
495
|
+
// Renders an AssistantFunctionCall (e.g. CodeEdit) as a tool name plus its
|
|
496
|
+
// parameters. Parameters is a Newtonsoft JObject, whose ToString() yields
|
|
497
|
+
// indented JSON, so no compile-time dependency on Newtonsoft is needed.
|
|
498
|
+
static string FormatFunctionCall(object call)
|
|
192
499
|
{
|
|
193
|
-
|
|
194
|
-
public readonly int PromptLine;
|
|
195
|
-
public readonly string PromptMessageId;
|
|
196
|
-
public readonly List<string> ResponseParts = new();
|
|
197
|
-
public readonly List<int> ResponseLines = new();
|
|
198
|
-
public string ResponseMessageId;
|
|
500
|
+
if (call == null) return null;
|
|
199
501
|
|
|
200
|
-
|
|
502
|
+
var name = GetMember(call, "FunctionId") as string;
|
|
503
|
+
var builder = new StringBuilder();
|
|
504
|
+
builder.Append("**Tool call: ").Append(string.IsNullOrEmpty(name) ? "(unknown)" : name).Append("**");
|
|
201
505
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
506
|
+
var parameters = GetMember(call, "Parameters")?.ToString();
|
|
507
|
+
if (!string.IsNullOrWhiteSpace(parameters))
|
|
508
|
+
builder.Append("\n\n```json\n").Append(parameters).Append("\n```");
|
|
509
|
+
|
|
510
|
+
return builder.ToString();
|
|
208
511
|
}
|
|
209
512
|
|
|
210
|
-
|
|
513
|
+
// --- Misc helpers -------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
static string DisplayTitle(string title)
|
|
516
|
+
=> string.IsNullOrEmpty(title) ? "(Untitled)" : title;
|
|
517
|
+
|
|
518
|
+
// Collapses CR/LF runs to single spaces so a value renders on one line.
|
|
519
|
+
static string SingleLine(string text)
|
|
520
|
+
=> string.IsNullOrEmpty(text)
|
|
521
|
+
? text
|
|
522
|
+
: System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ").Trim();
|
|
523
|
+
|
|
524
|
+
static string FormatRole(string role)
|
|
211
525
|
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
public readonly string Markdown;
|
|
526
|
+
if (string.IsNullOrEmpty(role)) return "Message";
|
|
527
|
+
return char.ToUpperInvariant(role[0]) + role.Substring(1);
|
|
528
|
+
}
|
|
216
529
|
|
|
217
|
-
|
|
530
|
+
static string FormatDate(long timestamp)
|
|
531
|
+
{
|
|
532
|
+
if (timestamp <= 0) return "";
|
|
533
|
+
try
|
|
534
|
+
{
|
|
535
|
+
var time = timestamp > 1_000_000_000_000L
|
|
536
|
+
? DateTimeOffset.FromUnixTimeMilliseconds(timestamp)
|
|
537
|
+
: DateTimeOffset.FromUnixTimeSeconds(timestamp);
|
|
538
|
+
return time.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
|
539
|
+
}
|
|
540
|
+
catch
|
|
218
541
|
{
|
|
219
|
-
|
|
220
|
-
MessageId = messageId;
|
|
221
|
-
LastMessage = lastMessage;
|
|
222
|
-
Markdown = markdown;
|
|
542
|
+
return "";
|
|
223
543
|
}
|
|
224
544
|
}
|
|
545
|
+
|
|
546
|
+
static string SanitizeFileName(string name)
|
|
547
|
+
{
|
|
548
|
+
if (string.IsNullOrEmpty(name)) return "conversation";
|
|
549
|
+
var invalid = Path.GetInvalidFileNameChars();
|
|
550
|
+
var sanitized = new string(name.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim();
|
|
551
|
+
return string.IsNullOrEmpty(sanitized) ? "conversation" : sanitized;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
static Type FindType(string fullName)
|
|
555
|
+
=> AppDomain.CurrentDomain.GetAssemblies()
|
|
556
|
+
.Select(a => a.GetType(fullName, false))
|
|
557
|
+
.FirstOrDefault(t => t != null);
|
|
558
|
+
|
|
559
|
+
// Reads a public/internal field or property by name from a live object
|
|
560
|
+
// (works on boxed structs too).
|
|
561
|
+
static object GetMember(object obj, string name)
|
|
562
|
+
{
|
|
563
|
+
if (obj == null) return null;
|
|
564
|
+
var type = obj.GetType();
|
|
565
|
+
var field = type.GetField(name, InstanceMembers);
|
|
566
|
+
if (field != null) return field.GetValue(obj);
|
|
567
|
+
return type.GetProperty(name, InstanceMembers)?.GetValue(obj);
|
|
568
|
+
}
|
|
225
569
|
}
|
|
226
570
|
|
|
227
571
|
} // namespace AIAssistantExtensions
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.unity-text-element {
|
|
2
|
+
-unity-font-definition: url("project://database/Packages/jp.keijiro.ai.assistant.extensions/Editor/NotoSansJP-Regular.ttf?fileID=12800000&guid=f7d2dd1a328194df9b71491ff2d07e2b&type=3#NotoSansJP-Regular");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.spacer {
|
|
6
|
+
flex-grow: 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.split {
|
|
10
|
+
flex-grow: 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#left-pane {
|
|
14
|
+
min-width: 150px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#right-pane {
|
|
18
|
+
min-width: 200px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.pane {
|
|
22
|
+
flex-grow: 1;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
padding: 4px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header {
|
|
28
|
+
-unity-font-style: bold;
|
|
29
|
+
margin-top: 2px;
|
|
30
|
+
margin-bottom: 2px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.preview-header {
|
|
34
|
+
flex-direction: row;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
align-items: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.toolcalls {
|
|
40
|
+
margin-bottom: 2px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.empty {
|
|
44
|
+
white-space: normal;
|
|
45
|
+
opacity: 0.6;
|
|
46
|
+
margin: 4px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.list {
|
|
50
|
+
flex-grow: 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.preview {
|
|
54
|
+
flex-grow: 1;
|
|
55
|
+
flex-shrink: 1;
|
|
56
|
+
min-height: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.preview .unity-base-text-field__input {
|
|
60
|
+
white-space: normal;
|
|
61
|
+
-unity-text-align: upper-left;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.row {
|
|
65
|
+
justify-content: center;
|
|
66
|
+
padding-left: 6px;
|
|
67
|
+
padding-right: 6px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.row-title {
|
|
71
|
+
-unity-font-style: bold;
|
|
72
|
+
text-overflow: ellipsis;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.row-date {
|
|
78
|
+
font-size: 10px;
|
|
79
|
+
opacity: 0.65;
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
fileFormatVersion: 2
|
|
2
|
+
guid: f6331e5dc7a84404baedc43bf6c5b522
|
|
3
|
+
ScriptedImporter:
|
|
4
|
+
internalIDToNameTable: []
|
|
5
|
+
externalObjects: {}
|
|
6
|
+
serializedVersion: 2
|
|
7
|
+
userData:
|
|
8
|
+
assetBundleName:
|
|
9
|
+
assetBundleVariant:
|
|
10
|
+
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
|
11
|
+
disableValidation: 0
|
|
12
|
+
unsupportedSelectorAction: 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
|
2
|
+
<Style src="project://database/Packages/jp.keijiro.ai.assistant.extensions/Editor/ConversationExtractorWindow.uss?fileID=7433441132597879392&guid=f6331e5dc7a84404baedc43bf6c5b522&type=3#ConversationExtractorWindow"/>
|
|
3
|
+
<uie:Toolbar>
|
|
4
|
+
<uie:ToolbarButton name="refresh-button" text="Refresh"/>
|
|
5
|
+
<ui:VisualElement class="spacer"/>
|
|
6
|
+
<uie:ToolbarButton name="copy-button" text="Copy"/>
|
|
7
|
+
<uie:ToolbarButton name="save-button" text="Save"/>
|
|
8
|
+
</uie:Toolbar>
|
|
9
|
+
<ui:TwoPaneSplitView name="split" orientation="Horizontal" fixed-pane-index="0" fixed-pane-initial-dimension="240" view-data-key="split" class="split">
|
|
10
|
+
<ui:VisualElement name="left-pane" class="pane">
|
|
11
|
+
<ui:Label name="list-header" class="header"/>
|
|
12
|
+
<ui:ListView name="conversation-list" view-data-key="list" fixed-item-height="38" selection-type="Single" class="list"/>
|
|
13
|
+
<ui:Label name="empty-label" class="empty"/>
|
|
14
|
+
</ui:VisualElement>
|
|
15
|
+
<ui:VisualElement name="right-pane" class="pane">
|
|
16
|
+
<ui:VisualElement name="preview-header" class="preview-header">
|
|
17
|
+
<ui:Label text="Extracted Markdown" class="header"/>
|
|
18
|
+
<ui:Toggle name="toolcalls-toggle" text="Tool Calls" class="toolcalls"/>
|
|
19
|
+
</ui:VisualElement>
|
|
20
|
+
<ui:TextField name="preview" multiline="true" readonly="true" vertical-scroller-visibility="Auto" focusable="false" class="preview"/>
|
|
21
|
+
</ui:VisualElement>
|
|
22
|
+
</ui:TwoPaneSplitView>
|
|
23
|
+
</ui:UXML>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fileFormatVersion: 2
|
|
2
|
+
guid: 7a48f083d5c894030ae036790fba1881
|
|
3
|
+
ScriptedImporter:
|
|
4
|
+
internalIDToNameTable: []
|
|
5
|
+
externalObjects: {}
|
|
6
|
+
serializedVersion: 2
|
|
7
|
+
userData:
|
|
8
|
+
assetBundleName:
|
|
9
|
+
assetBundleVariant:
|
|
10
|
+
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fileFormatVersion: 2
|
|
2
|
+
guid: d67de4ce2aaaa4897b5c8a3355889f53
|
|
3
|
+
ScriptedImporter:
|
|
4
|
+
internalIDToNameTable: []
|
|
5
|
+
externalObjects: {}
|
|
6
|
+
serializedVersion: 2
|
|
7
|
+
userData:
|
|
8
|
+
assetBundleName:
|
|
9
|
+
assetBundleVariant:
|
|
10
|
+
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jp.keijiro.ai.assistant.extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"displayName": "AI Assistant Extensions",
|
|
5
5
|
"description": "Custom Unity package that provides extensions for Unity AI Assistant.",
|
|
6
6
|
"unity": "6000.0",
|
|
7
7
|
"author": "Keijiro Takahashi",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"com.unity.ai.assistant": "2.
|
|
9
|
+
"com.unity.ai.assistant": "2.11.0-pre.1"
|
|
10
10
|
},
|
|
11
11
|
"changelogUrl": "https://github.com/keijiro/AIA-Extensions/blob/master/CHANGELOG.md",
|
|
12
12
|
"documentationUrl": "https://github.com/keijiro/AIA-Extensions",
|
|
13
13
|
"licensesUrl": "https://github.com/keijiro/AIA-Extensions/blob/master/LICENSE",
|
|
14
14
|
"license": "Unlicense",
|
|
15
15
|
"_upm": {
|
|
16
|
-
"changelog": "<b>
|
|
16
|
+
"changelog": "<b>Changed</b><br>- Reworked the Conversation Extractor to read conversations through the AI Assistant in-process API instead of parsing the relay log.<br>- Rebuilt the Conversation Extractor window with UI Toolkit, adding a resizable conversation list, per-conversation selection, a scrollable read-only transcript, and a toggle for including tool calls.<br>- Updated project and package metadata for AI Assistant 2.11.0-pre.1."
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"url": "git@github.com:keijiro/AIA-Extensions.git",
|
|
20
20
|
"type": "git",
|
|
21
|
-
"revision": "
|
|
21
|
+
"revision": "6a623e5a6d39c382845c3eb447fc3199bda740b3"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
using System;
|
|
2
|
-
using System.Collections;
|
|
3
|
-
using System.Collections.Generic;
|
|
4
|
-
using System.IO;
|
|
5
|
-
using System.Reflection;
|
|
6
|
-
using UnityEditor;
|
|
7
|
-
using UnityEngine;
|
|
8
|
-
|
|
9
|
-
namespace AIAssistantExtensions {
|
|
10
|
-
|
|
11
|
-
static class PackageSkillRegistrar
|
|
12
|
-
{
|
|
13
|
-
// The settings UI only displays Project/User/Internal source tags.
|
|
14
|
-
const string SkillTag = "Skills.User.Filesystem.Project";
|
|
15
|
-
const string PackageSkillTag =
|
|
16
|
-
"Skills.User.Filesystem.Project.Package.AIAssistantExtensions";
|
|
17
|
-
|
|
18
|
-
const string SkillPath =
|
|
19
|
-
"Packages/jp.keijiro.ai.assistant.extensions/Skills/GameViewCapture/SKILL.md";
|
|
20
|
-
|
|
21
|
-
static Action _rescanHandler;
|
|
22
|
-
static bool _registeredRescanHandler;
|
|
23
|
-
|
|
24
|
-
[InitializeOnLoadMethod]
|
|
25
|
-
static void Initialize()
|
|
26
|
-
{
|
|
27
|
-
RegisterPackageSkills();
|
|
28
|
-
SubscribeToSkillRescan();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
static void RegisterPackageSkills()
|
|
32
|
-
{
|
|
33
|
-
try
|
|
34
|
-
{
|
|
35
|
-
if (!File.Exists(SkillPath))
|
|
36
|
-
return;
|
|
37
|
-
|
|
38
|
-
var skill = CreateSkillFromFile(Path.GetFullPath(SkillPath));
|
|
39
|
-
if (skill == null)
|
|
40
|
-
return;
|
|
41
|
-
|
|
42
|
-
AddTag(skill, SkillTag);
|
|
43
|
-
AddTag(skill, PackageSkillTag);
|
|
44
|
-
RemoveExistingSkills();
|
|
45
|
-
AddSkills(skill);
|
|
46
|
-
}
|
|
47
|
-
catch (Exception ex)
|
|
48
|
-
{
|
|
49
|
-
Debug.LogError($"Failed to register AI Assistant extension skills: {ex.Message}");
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static void SubscribeToSkillRescan()
|
|
54
|
-
{
|
|
55
|
-
if (_registeredRescanHandler)
|
|
56
|
-
return;
|
|
57
|
-
|
|
58
|
-
var scannerType = FindType("Unity.AI.Assistant.Editor.SkillsScanner");
|
|
59
|
-
var eventInfo = scannerType?.GetEvent
|
|
60
|
-
("OnSkillsRescanned", BindingFlags.Static | BindingFlags.NonPublic);
|
|
61
|
-
if (eventInfo == null)
|
|
62
|
-
return;
|
|
63
|
-
|
|
64
|
-
_rescanHandler = () => EditorApplication.delayCall += RegisterPackageSkills;
|
|
65
|
-
var addMethod = eventInfo.GetAddMethod(true);
|
|
66
|
-
if (addMethod == null)
|
|
67
|
-
return;
|
|
68
|
-
|
|
69
|
-
addMethod.Invoke(null, new object[] { _rescanHandler });
|
|
70
|
-
_registeredRescanHandler = true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
static object CreateSkillFromFile(string fullPath)
|
|
74
|
-
{
|
|
75
|
-
var skillUtilsType = FindType("Unity.AI.Assistant.Skills.SkillUtils");
|
|
76
|
-
if (skillUtilsType == null)
|
|
77
|
-
throw new InvalidOperationException("AI Assistant SkillUtils type was not found.");
|
|
78
|
-
|
|
79
|
-
var method = skillUtilsType?.GetMethod
|
|
80
|
-
("CreateSkillFromFile", BindingFlags.Static | BindingFlags.NonPublic);
|
|
81
|
-
if (method == null)
|
|
82
|
-
throw new InvalidOperationException("AI Assistant SkillUtils.CreateSkillFromFile method was not found.");
|
|
83
|
-
|
|
84
|
-
return method.Invoke(null, new object[] { fullPath });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static void AddTag(object skill, string tag)
|
|
88
|
-
{
|
|
89
|
-
var method = skill.GetType().GetMethod
|
|
90
|
-
("WithTag", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
91
|
-
if (method == null)
|
|
92
|
-
throw new InvalidOperationException("AI Assistant SkillDefinition.WithTag method was not found.");
|
|
93
|
-
|
|
94
|
-
method.Invoke(skill, new object[] { tag });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
static void AddSkills(object skill)
|
|
98
|
-
{
|
|
99
|
-
var registryType = FindType("Unity.AI.Assistant.Skills.SkillsRegistry");
|
|
100
|
-
if (registryType == null)
|
|
101
|
-
throw new InvalidOperationException("AI Assistant SkillsRegistry type was not found.");
|
|
102
|
-
|
|
103
|
-
var method = registryType?.GetMethod
|
|
104
|
-
("AddSkills", BindingFlags.Static | BindingFlags.Public);
|
|
105
|
-
if (method == null)
|
|
106
|
-
throw new InvalidOperationException("AI Assistant SkillsRegistry.AddSkills method was not found.");
|
|
107
|
-
|
|
108
|
-
var listType = typeof(List<>).MakeGenericType(skill.GetType());
|
|
109
|
-
var list = (IList)Activator.CreateInstance(listType);
|
|
110
|
-
if (list == null)
|
|
111
|
-
throw new InvalidOperationException("Failed to create a SkillDefinition list.");
|
|
112
|
-
|
|
113
|
-
list.Add(skill);
|
|
114
|
-
|
|
115
|
-
method.Invoke(null, new object[] { list, null });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
static void RemoveExistingSkills()
|
|
119
|
-
{
|
|
120
|
-
var registryType = FindType("Unity.AI.Assistant.Skills.SkillsRegistry");
|
|
121
|
-
if (registryType == null)
|
|
122
|
-
return;
|
|
123
|
-
|
|
124
|
-
RemoveByTag(registryType, PackageSkillTag);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
static void RemoveByTag(Type registryType, string tag)
|
|
128
|
-
{
|
|
129
|
-
var method = registryType.GetMethod
|
|
130
|
-
("RemoveByTag", BindingFlags.Static | BindingFlags.Public);
|
|
131
|
-
if (method == null)
|
|
132
|
-
return;
|
|
133
|
-
|
|
134
|
-
method.Invoke(null, new object[] { tag });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
static Type FindType(string fullName)
|
|
138
|
-
{
|
|
139
|
-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
140
|
-
{
|
|
141
|
-
var type = assembly.GetType(fullName, false);
|
|
142
|
-
if (type != null)
|
|
143
|
-
return type;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
} // namespace AIAssistantExtensions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|