flowseeker 0.1.7

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 (82) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/COMPATIBILITY.md +281 -0
  3. package/LICENSE +21 -0
  4. package/README.md +375 -0
  5. package/dist/adapters/frameworkEdges.js +586 -0
  6. package/dist/agent/approvalPolicy.js +81 -0
  7. package/dist/agent/commandRunner.js +166 -0
  8. package/dist/agent/flowCommandRunner.js +124 -0
  9. package/dist/agent/mcpToolRunner.js +167 -0
  10. package/dist/auth/githubAuth.js +71 -0
  11. package/dist/auth/modelList.js +127 -0
  12. package/dist/auth/oauthHandler.js +377 -0
  13. package/dist/chat/nativeChatParticipant.js +616 -0
  14. package/dist/cli/mcpServer.js +383 -0
  15. package/dist/cli/runEvaluation.js +789 -0
  16. package/dist/cli/runReplay.js +481 -0
  17. package/dist/commands/checkHostCompatibility.js +149 -0
  18. package/dist/commands/copyAgentPrompt.js +52 -0
  19. package/dist/commands/copyContext.js +54 -0
  20. package/dist/commands/explainSelectionRelevance.js +57 -0
  21. package/dist/commands/findRelevantContext.js +127 -0
  22. package/dist/commands/openEvidence.js +49 -0
  23. package/dist/commands/openMcpConfig.js +81 -0
  24. package/dist/commands/openRelatedTests.js +54 -0
  25. package/dist/commands/rebuildIndex.js +45 -0
  26. package/dist/commands/runEvaluationSuite.js +323 -0
  27. package/dist/commands/runReplaySuite.js +228 -0
  28. package/dist/config/defaultConfig.js +72 -0
  29. package/dist/config/loadConfig.js +84 -0
  30. package/dist/config/loadConfigFromPath.js +60 -0
  31. package/dist/extension.js +513 -0
  32. package/dist/gateway/agentPrompts.js +176 -0
  33. package/dist/gateway/aiGateway.js +1255 -0
  34. package/dist/gateway/aiProviders.js +901 -0
  35. package/dist/gateway/contextExpansion.js +331 -0
  36. package/dist/gateway/editProposalStore.js +238 -0
  37. package/dist/gateway/planProposalStore.js +28 -0
  38. package/dist/index/cacheStore.js +51 -0
  39. package/dist/index/chunker.js +45 -0
  40. package/dist/index/dependencyExtractor.js +107 -0
  41. package/dist/index/fileDiscovery.js +177 -0
  42. package/dist/index/structuredExtractor.js +256 -0
  43. package/dist/index/workspaceIndex.js +518 -0
  44. package/dist/mcp/mcpConfig.js +154 -0
  45. package/dist/mcp/mcpProvider.js +109 -0
  46. package/dist/mcp/mcpTools.js +215 -0
  47. package/dist/pipeline/agentPrompt.js +79 -0
  48. package/dist/pipeline/agentPromptHeadless.js +85 -0
  49. package/dist/pipeline/contextBlueprint.js +346 -0
  50. package/dist/pipeline/contextPack.js +80 -0
  51. package/dist/pipeline/diffPreview.js +79 -0
  52. package/dist/pipeline/evaluationMetrics.js +154 -0
  53. package/dist/pipeline/evidenceGraph.js +389 -0
  54. package/dist/pipeline/feedback.js +215 -0
  55. package/dist/pipeline/fileGroups.js +84 -0
  56. package/dist/pipeline/fileScanner.js +866 -0
  57. package/dist/pipeline/nodeScan.js +219 -0
  58. package/dist/pipeline/ranker.js +563 -0
  59. package/dist/pipeline/responseLanguage.js +39 -0
  60. package/dist/pipeline/retrievalPlan.js +163 -0
  61. package/dist/pipeline/runHeadless.js +54 -0
  62. package/dist/pipeline/runPipeline.js +114 -0
  63. package/dist/pipeline/solvePacket.js +382 -0
  64. package/dist/pipeline/subsystem.js +257 -0
  65. package/dist/pipeline/taskUnderstanding.js +453 -0
  66. package/dist/pipeline/tokenSavings.js +146 -0
  67. package/dist/pipeline/universalScan.js +216 -0
  68. package/dist/pipeline/verifyAfterApply.js +233 -0
  69. package/dist/runtime/capabilities.js +71 -0
  70. package/dist/runtime/hostDetect.js +98 -0
  71. package/dist/runtime/hostTier.js +68 -0
  72. package/dist/runtime/statusBar.js +80 -0
  73. package/dist/skills/skillRegistry.js +208 -0
  74. package/dist/types.js +3 -0
  75. package/dist/ui/chatViewProvider.js +3899 -0
  76. package/dist/ui/resultTreeProvider.js +174 -0
  77. package/dist/usage/quotaTracker.js +358 -0
  78. package/dist/utils/async.js +30 -0
  79. package/dist/utils/logger.js +64 -0
  80. package/dist/utils/text.js +364 -0
  81. package/dist/utils/updateChecker.js +140 -0
  82. package/package.json +561 -0
@@ -0,0 +1,3899 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ChatViewProvider = void 0;
37
+ const vscode = __importStar(require("vscode"));
38
+ const aiProviders_1 = require("../gateway/aiProviders");
39
+ const commandRunner_1 = require("../agent/commandRunner");
40
+ const mcpToolRunner_1 = require("../agent/mcpToolRunner");
41
+ const approvalPolicy_1 = require("../agent/approvalPolicy");
42
+ const editProposalStore_1 = require("../gateway/editProposalStore");
43
+ const agentPrompts_1 = require("../gateway/agentPrompts");
44
+ const contextExpansion_1 = require("../gateway/contextExpansion");
45
+ const mcpConfig_1 = require("../mcp/mcpConfig");
46
+ const agentPrompt_1 = require("../pipeline/agentPrompt");
47
+ const diffPreview_1 = require("../pipeline/diffPreview");
48
+ const feedback_1 = require("../pipeline/feedback");
49
+ const fileGroups_1 = require("../pipeline/fileGroups");
50
+ const runPipeline_1 = require("../pipeline/runPipeline");
51
+ const tokenSavings_1 = require("../pipeline/tokenSavings");
52
+ const verifyAfterApply_1 = require("../pipeline/verifyAfterApply");
53
+ const logger_1 = require("../utils/logger");
54
+ const legacyHistoryStateKey = "flowseeker.chat.history";
55
+ const threadsStateKey = "flowseeker.chat.threads.v1";
56
+ const activeThreadStateKey = "flowseeker.chat.activeThreadId.v1";
57
+ const maxHistoryMessages = 80;
58
+ const maxChatThreads = 40;
59
+ class ChatViewProvider {
60
+ constructor(context, resultProvider, gateway, editStore, planStore, quotaTracker) {
61
+ this.context = context;
62
+ this.resultProvider = resultProvider;
63
+ this.gateway = gateway;
64
+ this.editStore = editStore;
65
+ this.planStore = planStore;
66
+ this.quotaTracker = quotaTracker;
67
+ const loaded = this.loadThreads();
68
+ this.threads = loaded.threads;
69
+ this.activeThreadId = loaded.activeThreadId;
70
+ this.context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((event) => {
71
+ if (event.affectsConfiguration("flowseeker.agent")) {
72
+ this.postAgentPolicyState();
73
+ }
74
+ }));
75
+ if (quotaTracker) {
76
+ this.context.subscriptions.push(quotaTracker.onDidChange(() => {
77
+ void this.refreshConnectionState();
78
+ }));
79
+ }
80
+ }
81
+ resolveWebviewView(webviewView) {
82
+ this.view = webviewView;
83
+ webviewView.webview.options = {
84
+ enableScripts: true
85
+ };
86
+ webviewView.webview.html = this.renderHtml(webviewView.webview);
87
+ webviewView.webview.onDidReceiveMessage((message) => {
88
+ void this.handleMessage(message);
89
+ });
90
+ }
91
+ async handleMessage(message) {
92
+ try {
93
+ switch (message.type) {
94
+ case "ready":
95
+ await this.restoreThreads();
96
+ await this.postConnectionState();
97
+ this.postAgentPolicyState();
98
+ return;
99
+ case "newThread":
100
+ await this.createNewThread();
101
+ return;
102
+ case "selectThread":
103
+ await this.selectThread(message.threadId);
104
+ return;
105
+ case "deleteThread":
106
+ await this.deleteThread(message.threadId);
107
+ return;
108
+ case "connect":
109
+ {
110
+ const input = toProviderInput(message);
111
+ let probeWarning;
112
+ try {
113
+ await this.gateway.configureProvider(input);
114
+ if (shouldProbeQuotaOnConnect(input.provider)) {
115
+ try {
116
+ await this.gateway.testProvider(input);
117
+ }
118
+ catch (error) {
119
+ probeWarning = formatQuotaProbeWarning(error);
120
+ }
121
+ }
122
+ await this.postConnectionState();
123
+ this.postGatewayFeedback(probeWarning ? "warning" : "success", probeWarning
124
+ ? `Profile saved, but quota probe failed. ${probeWarning}`
125
+ : "AI provider profile saved and active.");
126
+ }
127
+ catch (error) {
128
+ await this.postConnectionState();
129
+ this.postGatewayFeedback("error", formatErrorMessage(error));
130
+ }
131
+ }
132
+ return;
133
+ case "testProvider": {
134
+ try {
135
+ this.postGatewayFeedback("info", "Testing AI provider...");
136
+ const connection = await this.gateway.testProvider(toProviderInput(message));
137
+ this.postGatewayFeedback("success", `${connection.providerLabel ?? "AI provider"} test succeeded${connection.model ? ` with ${connection.model}` : ""}.`);
138
+ }
139
+ catch (error) {
140
+ this.postGatewayFeedback("error", formatErrorMessage(error));
141
+ }
142
+ return;
143
+ }
144
+ case "fetchModels": {
145
+ const input = toProviderInput(message);
146
+ const models = await this.gateway.listModelsForInput(input);
147
+ this.post({ type: "models", provider: input.provider, model: input.model ?? "", models });
148
+ return;
149
+ }
150
+ case "disconnect":
151
+ try {
152
+ await this.gateway.disconnect();
153
+ await this.postConnectionState();
154
+ this.postGatewayFeedback("info", "AI provider disconnected. Local retrieval remains available.");
155
+ }
156
+ catch (error) {
157
+ await this.postConnectionState();
158
+ this.postGatewayFeedback("error", formatErrorMessage(error));
159
+ }
160
+ return;
161
+ case "selectProviderProfile": {
162
+ try {
163
+ const connection = await this.gateway.setActiveProviderProfile(message.profileId);
164
+ await this.postConnectionState();
165
+ this.postGatewayFeedback("success", `Switched to ${connection.providerLabel ?? "AI provider"}${connection.model ? ` / ${connection.model}` : ""}.`);
166
+ }
167
+ catch (error) {
168
+ await this.postConnectionState();
169
+ this.postGatewayFeedback("error", formatErrorMessage(error));
170
+ }
171
+ return;
172
+ }
173
+ case "removeProviderProfile":
174
+ try {
175
+ await this.gateway.removeProviderProfile(message.profileId);
176
+ await this.postConnectionState();
177
+ this.postGatewayFeedback("info", "AI provider profile removed.");
178
+ }
179
+ catch (error) {
180
+ await this.postConnectionState();
181
+ this.postGatewayFeedback("error", formatErrorMessage(error));
182
+ }
183
+ return;
184
+ case "ask":
185
+ await this.ask(message.task, message.mode);
186
+ return;
187
+ case "feedback":
188
+ await this.saveFeedback(message.runId, message.kind);
189
+ return;
190
+ case "copyAgentPrompt":
191
+ await this.copyAgentPrompt();
192
+ return;
193
+ case "copyTokenProof":
194
+ await this.copyTokenProof();
195
+ return;
196
+ case "approvePlan":
197
+ await this.approvePlan(message.planId);
198
+ return;
199
+ case "openDiff":
200
+ await this.openDiff(message.proposalId);
201
+ return;
202
+ case "applyEdits":
203
+ await this.applyEdits(message.proposalId);
204
+ return;
205
+ case "verifyFiles":
206
+ await this.verifyFiles(message.files);
207
+ return;
208
+ case "runAgentCommand":
209
+ await this.runAgentCommand(message.command);
210
+ return;
211
+ case "runMcpTool":
212
+ await this.runMcpTool(message.tool);
213
+ return;
214
+ case "openAgentSettings":
215
+ await this.openAgentSettings();
216
+ return;
217
+ case "updateAgentPolicy":
218
+ await this.updateAgentPolicy(message);
219
+ return;
220
+ case "cancel":
221
+ await this.cancelActiveRequest();
222
+ return;
223
+ case "startOAuth":
224
+ await vscode.commands.executeCommand("flowseeker.startOAuth", message.provider);
225
+ return;
226
+ case "openFile":
227
+ await this.openFile(message.file);
228
+ return;
229
+ case "openMcpConfig":
230
+ await vscode.commands.executeCommand("flowseeker.openMcpConfig");
231
+ return;
232
+ }
233
+ }
234
+ catch (error) {
235
+ (0, logger_1.logError)("FlowSeeker chat action failed.", error);
236
+ const text = error instanceof Error ? error.message : String(error);
237
+ await this.postChat({ kind: "error", text });
238
+ }
239
+ }
240
+ async ask(task, mode) {
241
+ const cleanTask = task.trim();
242
+ if (!cleanTask) {
243
+ this.post({ type: "error", text: "Enter a task first." });
244
+ return;
245
+ }
246
+ // Chat mode: bypass FlowSeeker retrieval, go straight to AI provider
247
+ if (mode === "chat") {
248
+ const conn = await this.gateway.getConnection();
249
+ if (!conn.connected) {
250
+ await this.postChat({ kind: "error", text: "Connect an AI provider before using Chat mode." }, this.activeThread().id);
251
+ return;
252
+ }
253
+ this.askCancellation?.cancel();
254
+ this.askCancellation?.dispose();
255
+ const requestCancellation = new vscode.CancellationTokenSource();
256
+ this.askCancellation = requestCancellation;
257
+ const requestThreadId = this.activeThread().id;
258
+ try {
259
+ await this.postChat({ kind: "user", text: cleanTask }, requestThreadId);
260
+ if (requestCancellation.token.isCancellationRequested) {
261
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
262
+ return;
263
+ }
264
+ await this.postChat({ kind: "status", text: "Asking AI provider..." }, requestThreadId);
265
+ const abortController = new AbortController();
266
+ const abortSubscription = requestCancellation.token.onCancellationRequested(() => abortController.abort());
267
+ try {
268
+ this.post({ type: "assistantStart" });
269
+ const chatAnswer = await this.gateway.askDirectChatStream(cleanTask, (chunk) => this.post({ type: "assistantDelta", text: chunk }), abortController.signal, conn.provider);
270
+ if (requestCancellation.token.isCancellationRequested) {
271
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
272
+ return;
273
+ }
274
+ await this.postChat({ kind: "assistant", text: chatAnswer.message }, requestThreadId);
275
+ }
276
+ finally {
277
+ abortSubscription.dispose();
278
+ }
279
+ }
280
+ catch (error) {
281
+ if (requestCancellation.token.isCancellationRequested || isCancellationError(error)) {
282
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
283
+ return;
284
+ }
285
+ const msg = error instanceof Error ? error.message : String(error);
286
+ await this.postChat({ kind: "error", text: msg }, requestThreadId);
287
+ }
288
+ finally {
289
+ if (this.askCancellation === requestCancellation) {
290
+ this.askCancellation = undefined;
291
+ }
292
+ requestCancellation.dispose();
293
+ }
294
+ return;
295
+ }
296
+ const folders = vscode.workspace.workspaceFolders;
297
+ if (!folders || folders.length === 0) {
298
+ await this.postChat({ kind: "error", text: "Open a workspace folder before using FlowSeeker Chat." });
299
+ return;
300
+ }
301
+ this.askCancellation?.cancel();
302
+ this.askCancellation?.dispose();
303
+ const requestCancellation = new vscode.CancellationTokenSource();
304
+ this.askCancellation = requestCancellation;
305
+ const requestThreadId = this.activeThread().id;
306
+ try {
307
+ await this.postChat({ kind: "user", text: cleanTask }, requestThreadId);
308
+ await this.postChat({ kind: "status", text: "Finding relevant files before contacting the agent..." }, requestThreadId);
309
+ (0, logger_1.logInfo)(`Chat scan started for task: ${cleanTask}`);
310
+ const result = await vscode.window.withProgress({
311
+ location: vscode.ProgressLocation.Notification,
312
+ title: "FlowSeeker is preparing agent context",
313
+ cancellable: true
314
+ }, async (progress, progressToken) => {
315
+ progress.report({ message: "Retrieving relevant context..." });
316
+ const linkedCancellation = new vscode.CancellationTokenSource();
317
+ const disposables = [
318
+ progressToken.onCancellationRequested(() => linkedCancellation.cancel()),
319
+ requestCancellation.token.onCancellationRequested(() => linkedCancellation.cancel())
320
+ ];
321
+ try {
322
+ return await (0, runPipeline_1.runFlowSeeker)(cleanTask, { progress, token: linkedCancellation.token });
323
+ }
324
+ finally {
325
+ disposables.forEach((disposable) => disposable.dispose());
326
+ linkedCancellation.dispose();
327
+ }
328
+ });
329
+ if (requestCancellation.token.isCancellationRequested) {
330
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
331
+ return;
332
+ }
333
+ this.resultProvider.setResult(result);
334
+ const fileSummary = formatFileSummary(result);
335
+ const tokenMetric = formatTokenMetric(result);
336
+ const workspace = folders[0];
337
+ const feedback = (0, feedback_1.createFeedbackSnapshot)(result, workspace.uri.fsPath, workspace.name);
338
+ await this.postChat({
339
+ kind: "retrieval",
340
+ text: fileSummary,
341
+ metric: tokenMetric,
342
+ feedback,
343
+ files: (0, fileGroups_1.aggregateEvidenceFiles)(result.units)
344
+ .slice(0, 12)
345
+ .map((group) => ({
346
+ path: group.relativePath,
347
+ role: group.role,
348
+ score: group.score.toFixed(1),
349
+ evidence: group.unitCount
350
+ }))
351
+ }, requestThreadId);
352
+ const connection = await this.gateway.getConnection();
353
+ if (!connection.connected) {
354
+ await this.postChat({
355
+ kind: "assistant",
356
+ text: "Retrieval is ready. Configure an AI provider to ask an agent, or copy the agent prompt and paste it into your current AI tool."
357
+ }, requestThreadId);
358
+ return;
359
+ }
360
+ await this.postChat({ kind: "status", text: mode === "auto" ? "Asking AI provider for a reviewable plan..." : "Asking AI provider for guidance..." }, requestThreadId);
361
+ if (requestCancellation.token.isCancellationRequested) {
362
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
363
+ return;
364
+ }
365
+ let answer;
366
+ if (mode === "auto") {
367
+ answer = await this.gateway.ask(result, mode);
368
+ }
369
+ else {
370
+ const abortController = new AbortController();
371
+ const abortSubscription = requestCancellation.token.onCancellationRequested(() => abortController.abort());
372
+ try {
373
+ this.post({ type: "assistantStart" });
374
+ answer = await this.gateway.askStream(result, mode, (chunk) => this.post({ type: "assistantDelta", text: chunk }), abortController.signal);
375
+ }
376
+ finally {
377
+ abortSubscription.dispose();
378
+ }
379
+ }
380
+ if (requestCancellation.token.isCancellationRequested) {
381
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
382
+ return;
383
+ }
384
+ if (mode === "auto" && !answer.edits?.length && (0, contextExpansion_1.isMissingContextRequest)(answer.message)) {
385
+ const expanded = await this.expandContextAndRetryPlan(result, answer, requestThreadId);
386
+ if (expanded) {
387
+ answer = expanded;
388
+ }
389
+ if (requestCancellation.token.isCancellationRequested) {
390
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
391
+ return;
392
+ }
393
+ }
394
+ const editProposalId = answer.edits?.length ? this.editStore.store(answer.edits) : undefined;
395
+ const planProposalId = mode === "auto" && !answer.edits?.length && (0, contextExpansion_1.isReviewablePlan)(answer.message)
396
+ ? this.planStore.store(result, answer.message)
397
+ : undefined;
398
+ const diff = answer.edits?.length ? (0, diffPreview_1.renderDiffPreviewMarkdown)(answer.edits) : "";
399
+ const planWarning = mode === "auto" && !answer.edits?.length && !planProposalId
400
+ ? "FlowSeeker did not receive a reviewable implementation plan, so approval is disabled for this response."
401
+ : "";
402
+ await this.postChat({
403
+ kind: "assistant",
404
+ text: [
405
+ answer.patch ? `${answer.message}\n\nPatch proposal:\n\n${answer.patch}` : answer.message,
406
+ diff,
407
+ planWarning
408
+ ].filter(Boolean).join("\n\n"),
409
+ proposal: editProposalId
410
+ ? {
411
+ id: editProposalId,
412
+ kind: "edits",
413
+ edits: answer.edits?.map((edit) => ({
414
+ file: edit.file,
415
+ range: edit.range ? `${edit.range.startLine}:${edit.range.startColumn ?? 1}-${edit.range.endLine}:${edit.range.endColumn ?? 1}` : edit.replaceWholeFile ? "whole file" : "missing range"
416
+ }))
417
+ }
418
+ : planProposalId
419
+ ? {
420
+ id: planProposalId,
421
+ kind: "plan"
422
+ }
423
+ : undefined,
424
+ commands: answer.commands,
425
+ mcpTools: answer.mcpTools
426
+ }, requestThreadId);
427
+ }
428
+ catch (error) {
429
+ if (requestCancellation.token.isCancellationRequested || isCancellationError(error)) {
430
+ await this.postChat({ kind: "status", text: "Cancelled." }, requestThreadId);
431
+ return;
432
+ }
433
+ throw error;
434
+ }
435
+ finally {
436
+ if (this.askCancellation === requestCancellation) {
437
+ this.askCancellation = undefined;
438
+ }
439
+ requestCancellation.dispose();
440
+ }
441
+ }
442
+ async cancelActiveRequest() {
443
+ if (!this.askCancellation) {
444
+ return;
445
+ }
446
+ this.askCancellation.cancel();
447
+ await this.postChat({ kind: "status", text: "Cancelling current request..." });
448
+ }
449
+ async applyEdits(proposalId) {
450
+ const summary = await this.editStore.apply(proposalId);
451
+ const policy = (0, approvalPolicy_1.getApprovalPolicy)();
452
+ const parts = [`Applied ${summary.editCount} edit(s) to ${summary.files.length} file(s).`];
453
+ if (summary.failedFiles.length > 0) {
454
+ parts.push(`\n**${summary.failedFiles.length} file(s) failed:**`);
455
+ for (const failed of summary.failedFiles) {
456
+ parts.push(`- \`${failed.file}\`: ${failed.reason}`);
457
+ }
458
+ }
459
+ await this.postChat({
460
+ kind: summary.failedFiles.length > 0 ? "error" : "assistant",
461
+ text: [
462
+ parts.join("\n"),
463
+ policy.safeCommands || policy.allCommands ? "Auto-running verification because safe command auto-approval is enabled." : "Run verification when you are ready."
464
+ ].join(" "),
465
+ trace: [
466
+ "Structured edits applied",
467
+ ...(summary.failedFiles.length > 0 ? [`${summary.failedFiles.length} file(s) failed to apply`] : []),
468
+ policy.safeCommands || policy.allCommands ? "Verification auto-started by policy" : "Verification waiting for user"
469
+ ],
470
+ verificationFiles: summary.files
471
+ });
472
+ if (policy.safeCommands || policy.allCommands) {
473
+ await this.runVerificationForChat(summary.files);
474
+ }
475
+ }
476
+ async expandContextAndRetryPlan(result, answer, threadId) {
477
+ const paths = (0, contextExpansion_1.selectContextExpansionPaths)(answer.message, result);
478
+ if (paths.length === 0) {
479
+ await this.postChat({
480
+ kind: "status",
481
+ text: "The AI requested more context, but FlowSeeker could not identify a bounded file set to read."
482
+ }, threadId);
483
+ return undefined;
484
+ }
485
+ await this.postChat({
486
+ kind: "status",
487
+ text: `AI requested more context. Reading ${paths.length} bounded file(s) and retrying the plan...`
488
+ }, threadId);
489
+ const expandedContext = await (0, contextExpansion_1.readWorkspaceContextFiles)(paths);
490
+ if (!expandedContext.trim()) {
491
+ await this.postChat({
492
+ kind: "status",
493
+ text: "FlowSeeker could not read the requested context files safely."
494
+ }, threadId);
495
+ return undefined;
496
+ }
497
+ return this.gateway.askWithInstruction(result, "auto", (0, contextExpansion_1.createExpandedPlanInstruction)(answer.message, expandedContext), { stage: "expanded_plan", requestedFiles: paths });
498
+ }
499
+ async expandContextAndRetryApprovedEdits(result, approvedPlan, answer, threadId) {
500
+ const paths = (0, contextExpansion_1.selectContextExpansionPaths)(answer.message, result);
501
+ if (paths.length === 0) {
502
+ await this.postChat({
503
+ kind: "status",
504
+ text: "The AI requested more edit context, but FlowSeeker could not identify a bounded file set to read."
505
+ }, threadId);
506
+ return undefined;
507
+ }
508
+ await this.postChat({
509
+ kind: "status",
510
+ text: `AI requested more edit context. Reading ${paths.length} bounded file(s) and retrying structured edits...`
511
+ }, threadId);
512
+ const expandedContext = await (0, contextExpansion_1.readWorkspaceContextFiles)(paths);
513
+ if (!expandedContext.trim()) {
514
+ await this.postChat({
515
+ kind: "status",
516
+ text: "FlowSeeker could not read the requested edit context files safely."
517
+ }, threadId);
518
+ return undefined;
519
+ }
520
+ return this.gateway.askWithInstruction(result, "auto", (0, contextExpansion_1.createExpandedApprovedEditInstruction)(approvedPlan, answer.message, expandedContext), { stage: "expanded_approved_edits", requestedFiles: paths, approvedPlan });
521
+ }
522
+ async approvePlan(planId) {
523
+ const proposal = this.planStore.get(planId);
524
+ if (!proposal) {
525
+ await this.postChat({ kind: "error", text: "This plan is no longer available. Run the request again to regenerate it." });
526
+ return;
527
+ }
528
+ const connection = await this.gateway.getConnection();
529
+ if (!connection.connected) {
530
+ await this.postChat({ kind: "error", text: "Connect an AI provider before generating edits from the approved plan." });
531
+ return;
532
+ }
533
+ await this.postChat({ kind: "status", text: "Plan approved. Asking AI provider for structured edits..." });
534
+ let answer = await this.gateway.askWithInstruction(proposal.result, "auto", (0, agentPrompts_1.createApprovedEditInstruction)(proposal.plan), { stage: "approved_edits", approvedPlan: proposal.plan });
535
+ if (!answer.edits?.length && (0, contextExpansion_1.isMissingContextRequest)(answer.message)) {
536
+ const expanded = await this.expandContextAndRetryApprovedEdits(proposal.result, proposal.plan, answer, this.activeThread().id);
537
+ if (expanded) {
538
+ answer = expanded;
539
+ }
540
+ }
541
+ if (!answer.edits?.length) {
542
+ await this.postChat({
543
+ kind: "error",
544
+ text: [
545
+ "The AI provider did not return structured edits FlowSeeker can apply.",
546
+ "",
547
+ "**What happened:** The provider responded but the output was not in the expected JSON format with file edits.",
548
+ "",
549
+ "**What to do:**",
550
+ "- Try expanding context first (the plan may have mentioned files FlowSeeker did not retrieve yet).",
551
+ "- Switch to Guide mode for a text explanation instead of structured edits.",
552
+ "- Try a different model — some smaller models struggle with structured JSON output.",
553
+ "",
554
+ "**Expected JSON shape:**",
555
+ '```json',
556
+ '{"message":"explanation","edits":[{"file":"relative/path","range":{"startLine":1,"endLine":1},"text":"replacement"}],"commands":[],"mcpTools":[]}',
557
+ '```',
558
+ answer.message ? `**Provider response preview:**\n\`\`\`\n${(0, contextExpansion_1.truncateForAgent)(answer.message, 1200)}\n\`\`\`` : ""
559
+ ].filter(Boolean).join("\n"),
560
+ trace: [
561
+ "Plan approved",
562
+ "AI response did not contain valid structured edits",
563
+ "No workspace files were changed"
564
+ ],
565
+ commands: answer.commands,
566
+ mcpTools: answer.mcpTools
567
+ });
568
+ return;
569
+ }
570
+ const editProposalId = answer.edits?.length ? this.editStore.store(answer.edits, proposal.allowedFiles) : undefined;
571
+ const diff = answer.edits?.length ? (0, diffPreview_1.renderDiffPreviewMarkdown)(answer.edits) : "";
572
+ if (editProposalId && (0, approvalPolicy_1.getApprovalPolicy)().editWorkspace) {
573
+ const summary = await this.editStore.apply(editProposalId);
574
+ const policy = (0, approvalPolicy_1.getApprovalPolicy)();
575
+ await this.postChat({
576
+ kind: "assistant",
577
+ text: [
578
+ answer.message,
579
+ diff,
580
+ `Auto-approved and applied ${summary.editCount} edit(s) to ${summary.fileCount} file(s).`,
581
+ policy.safeCommands || policy.allCommands ? "Auto-running verification because safe command auto-approval is enabled." : "Run verification when you are ready."
582
+ ].filter(Boolean).join("\n\n"),
583
+ trace: [
584
+ "Plan approved",
585
+ "Structured edits generated",
586
+ "Edits applied by auto-approve policy",
587
+ policy.safeCommands || policy.allCommands ? "Verification auto-started by policy" : "Verification waiting for user"
588
+ ],
589
+ commands: answer.commands,
590
+ mcpTools: answer.mcpTools,
591
+ verificationFiles: summary.files
592
+ });
593
+ if (policy.safeCommands || policy.allCommands) {
594
+ await this.runVerificationForChat(summary.files);
595
+ }
596
+ return;
597
+ }
598
+ await this.postChat({
599
+ kind: "assistant",
600
+ text: [
601
+ answer.message,
602
+ diff,
603
+ editProposalId ? "Review the proposed diff before applying. Files are scope-locked to the approved plan." : ""
604
+ ].filter(Boolean).join("\n\n"),
605
+ proposal: editProposalId
606
+ ? {
607
+ id: editProposalId,
608
+ kind: "edits",
609
+ edits: answer.edits?.map((edit) => ({
610
+ file: edit.file,
611
+ range: edit.range ? `${edit.range.startLine}:${edit.range.startColumn ?? 1}-${edit.range.endLine}:${edit.range.endColumn ?? 1}` : edit.replaceWholeFile ? "whole file" : "missing range"
612
+ }))
613
+ }
614
+ : undefined,
615
+ commands: answer.commands,
616
+ mcpTools: answer.mcpTools
617
+ });
618
+ }
619
+ async openDiff(proposalId) {
620
+ const folders = vscode.workspace.workspaceFolders;
621
+ if (!folders?.[0]) {
622
+ await this.postChat({ kind: "error", text: "Open a workspace folder before previewing edits." });
623
+ return;
624
+ }
625
+ const files = this.editStore.listFiles(proposalId);
626
+ if (files.length === 0) {
627
+ await this.postChat({ kind: "error", text: "Diff preview is no longer available." });
628
+ return;
629
+ }
630
+ const file = files.length === 1
631
+ ? files[0]
632
+ : await vscode.window.showQuickPick(files, {
633
+ title: "FlowSeeker Diff",
634
+ placeHolder: "Choose a file to preview"
635
+ });
636
+ if (!file) {
637
+ return;
638
+ }
639
+ const preview = await this.editStore.previewFile(folders[0].uri, proposalId, file);
640
+ const document = await vscode.workspace.openTextDocument({
641
+ language: preview.languageId,
642
+ content: preview.text
643
+ });
644
+ await vscode.commands.executeCommand("vscode.diff", preview.originalUri, document.uri, `FlowSeeker Diff: ${preview.file}`, { preview: false, viewColumn: vscode.ViewColumn.Beside });
645
+ }
646
+ async verifyFiles(files) {
647
+ const changedFiles = [...new Set((files ?? []).filter(Boolean))];
648
+ if (changedFiles.length === 0) {
649
+ await this.postChat({ kind: "error", text: "No changed files were provided for verification." });
650
+ return;
651
+ }
652
+ await this.runVerificationForChat(changedFiles);
653
+ }
654
+ async runAgentCommand(command) {
655
+ if (!command?.command?.trim()) {
656
+ await this.postChat({ kind: "error", text: "No agent command was provided." });
657
+ return;
658
+ }
659
+ await this.postChat({
660
+ kind: "status",
661
+ text: `Running agent command: ${command.command}`,
662
+ trace: ["Agent command requested"]
663
+ });
664
+ const run = await (0, commandRunner_1.runAgentCommandProposal)(command);
665
+ if (!run) {
666
+ await this.postChat({
667
+ kind: "status",
668
+ text: "Agent command was cancelled.",
669
+ trace: ["Agent command cancelled"]
670
+ });
671
+ return;
672
+ }
673
+ await this.postChat({
674
+ kind: run.exitCode === 0 ? "assistant" : "error",
675
+ text: `${(0, commandRunner_1.summarizeAgentCommandRun)(run)} Review the command result document opened beside the editor.`,
676
+ trace: [
677
+ run.classification.safe ? "Command classified as safe" : "Command required explicit approval",
678
+ "Command completed"
679
+ ]
680
+ });
681
+ }
682
+ async runMcpTool(tool) {
683
+ if (!tool?.tool?.trim()) {
684
+ await this.postChat({ kind: "error", text: "No MCP/tool proposal was provided." });
685
+ return;
686
+ }
687
+ await this.postChat({
688
+ kind: "status",
689
+ text: `Invoking MCP/tool: ${tool.tool}`,
690
+ trace: ["MCP/tool invocation requested"]
691
+ });
692
+ const run = await (0, mcpToolRunner_1.runMcpToolProposal)(tool);
693
+ if (!run) {
694
+ await this.postChat({
695
+ kind: "status",
696
+ text: "MCP/tool invocation was cancelled or unavailable.",
697
+ trace: ["MCP/tool invocation not completed"]
698
+ });
699
+ return;
700
+ }
701
+ await this.postChat({
702
+ kind: "assistant",
703
+ text: `${(0, mcpToolRunner_1.summarizeMcpToolRun)(run)} Review the tool result document opened beside the editor.`,
704
+ trace: ["MCP/tool invoked", "Tool result captured"]
705
+ });
706
+ }
707
+ async runVerificationForChat(files) {
708
+ const changedFiles = [...new Set((files ?? []).filter(Boolean))];
709
+ await this.postChat({ kind: "status", text: `Running verification for ${changedFiles.length} changed file(s)...` });
710
+ const run = await (0, verifyAfterApply_1.verifyAfterEdits)(changedFiles);
711
+ await this.postChat({
712
+ kind: run ? "assistant" : "error",
713
+ text: run
714
+ ? `${(0, verifyAfterApply_1.summarizeVerificationRun)(run)} Review the results document opened beside the editor.`
715
+ : "Verification could not complete. Check the FlowSeeker log for details.",
716
+ trace: [
717
+ "Verification commands detected",
718
+ run ? "Verification completed" : "Verification failed to run"
719
+ ],
720
+ verificationFiles: changedFiles
721
+ });
722
+ }
723
+ async saveFeedback(runId, rawKind) {
724
+ const kind = (0, feedback_1.normalizeFeedbackKind)(rawKind);
725
+ if (!kind) {
726
+ await this.postChat({ kind: "error", text: "Feedback could not be saved because the feedback type was not recognized." });
727
+ return;
728
+ }
729
+ const snapshot = this.findFeedbackSnapshot(runId);
730
+ if (!snapshot) {
731
+ await this.postChat({ kind: "error", text: "Feedback could not be saved because this retrieval snapshot is no longer available." });
732
+ return;
733
+ }
734
+ const note = kind === "relevant"
735
+ ? undefined
736
+ : await vscode.window.showInputBox({
737
+ title: "FlowSeeker Feedback",
738
+ prompt: "Optional: what was missing or wrong?",
739
+ placeHolder: "Example: missing route file, wrong module, AI needed model/schema...",
740
+ ignoreFocusOut: true
741
+ });
742
+ const record = (0, feedback_1.createFeedbackRecord)(snapshot, kind, { source: "sidebar_chat", note });
743
+ const savedPath = await (0, feedback_1.writeFeedbackRecord)(snapshot.workspacePath, record);
744
+ (0, logger_1.logInfo)(`FlowSeeker feedback saved: ${savedPath}`);
745
+ const replayHint = `Run "FlowSeeker: Run Real-World Replay" to include this feedback in replay metrics.`;
746
+ await this.postChat({ kind: "assistant", text: `Feedback saved: ${(0, feedback_1.feedbackKindLabel)(kind)}. Saved to ${savedPath}. ${replayHint}` });
747
+ }
748
+ async copyAgentPrompt() {
749
+ const result = this.resultProvider.getResult();
750
+ if (!result) {
751
+ await this.postChat({ kind: "error", text: "Run a chat request first." });
752
+ return;
753
+ }
754
+ await vscode.env.clipboard.writeText((0, agentPrompt_1.createAgentSolvePrompt)(result, this.resultProvider.getSelectedUnits()));
755
+ await this.postChat({ kind: "assistant", text: "Agent prompt copied to clipboard." });
756
+ }
757
+ async copyTokenProof() {
758
+ const result = this.resultProvider.getResult();
759
+ if (!result) {
760
+ await this.postChat({ kind: "error", text: "Run a chat request first." });
761
+ return;
762
+ }
763
+ const task = result.profile?.intent ?? "Unknown task";
764
+ const summary = (0, tokenSavings_1.formatTokenProofSummary)(task, result.tokenSavings, result.contextCoverage);
765
+ await vscode.env.clipboard.writeText(summary);
766
+ await this.postChat({ kind: "assistant", text: "Token proof copied to clipboard." });
767
+ }
768
+ async openFile(relativePath) {
769
+ const folders = vscode.workspace.workspaceFolders;
770
+ if (!folders?.[0]) {
771
+ return;
772
+ }
773
+ const uri = (0, editProposalStore_1.resolveWorkspaceFile)(folders[0].uri, relativePath);
774
+ await vscode.window.showTextDocument(uri, { preview: true });
775
+ }
776
+ async postConnectionState() {
777
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
778
+ let connection = { connected: false };
779
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
780
+ let profilesState = { profiles: [] };
781
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
782
+ let models = [];
783
+ let usagePayload = undefined;
784
+ let quotaDebug = -1;
785
+ try {
786
+ connection = await this.gateway.getConnection();
787
+ }
788
+ catch { /* keep defaults */ }
789
+ try {
790
+ profilesState = await this.gateway.listProviderProfiles();
791
+ }
792
+ catch { /* keep defaults */ }
793
+ const providerId = connection.provider;
794
+ try {
795
+ if (providerId)
796
+ models = await this.gateway.listModels(providerId);
797
+ }
798
+ catch { /* keep defaults */ }
799
+ try {
800
+ if (this.quotaTracker) {
801
+ const usageSummary = this.quotaTracker.getSummary();
802
+ usagePayload = this.postUsagePayload(usageSummary ?? null, profilesState.profiles);
803
+ quotaDebug = usageSummary?.totalRequests ?? -1;
804
+ }
805
+ }
806
+ catch { /* keep defaults */ }
807
+ this.post({
808
+ type: "connection",
809
+ profileId: connection.profileId ?? profilesState.activeProfileId ?? "",
810
+ connected: connection.connected,
811
+ endpoint: connection.endpoint ?? "",
812
+ provider: connection.provider ?? "",
813
+ providerLabel: connection.providerLabel ?? "",
814
+ model: connection.model ?? "",
815
+ baseUrl: connection.baseUrl ?? connection.endpoint ?? "",
816
+ requiresApiKey: connection.requiresApiKey ?? true,
817
+ activeProfileId: profilesState.activeProfileId ?? "",
818
+ profiles: profilesState.profiles,
819
+ models,
820
+ usage: usagePayload,
821
+ quotaDebug
822
+ });
823
+ }
824
+ async refreshConnectionState() {
825
+ await this.postConnectionState();
826
+ }
827
+ postAgentPolicyState() {
828
+ void this.postAgentPolicyStateAsync();
829
+ }
830
+ async postAgentPolicyStateAsync() {
831
+ const policy = (0, approvalPolicy_1.getApprovalPolicy)();
832
+ const rules = policy.rules.map((rule) => rule.trim()).filter(Boolean);
833
+ const servers = await (0, mcpConfig_1.loadMcpServerConfigs)().catch(() => []);
834
+ const enabledServers = servers.filter((server) => server.enabled !== false);
835
+ const mcpServerCount = enabledServers.length;
836
+ const mcpServers = servers.map((server) => ({
837
+ name: server.name,
838
+ type: server.type,
839
+ enabled: server.enabled !== false,
840
+ valid: server.enabled === false ? true : server.type === "http" ? Boolean(server.url?.trim()) : Boolean(server.command?.trim())
841
+ }));
842
+ this.post({
843
+ type: "agentPolicy",
844
+ readWorkspace: policy.readWorkspace,
845
+ editWorkspace: policy.editWorkspace,
846
+ safeCommands: policy.safeCommands,
847
+ allCommands: policy.allCommands,
848
+ mcpTools: policy.mcpTools,
849
+ mcpServerCount,
850
+ mcpServers,
851
+ rules,
852
+ rulesCount: rules.length
853
+ });
854
+ }
855
+ async openAgentSettings() {
856
+ await vscode.commands.executeCommand("workbench.action.openSettings", "@ext:everestt2806.flowseeker agent");
857
+ }
858
+ async updateAgentPolicy(message) {
859
+ const current = (0, approvalPolicy_1.getApprovalPolicy)();
860
+ if (message.allCommands && !current.allCommands) {
861
+ const action = await vscode.window.showWarningMessage("FlowSeeker will be allowed to auto-approve non-destructive arbitrary commands. Destructive commands still require explicit approval.", { modal: true }, "Enable", "Cancel");
862
+ if (action !== "Enable") {
863
+ this.postAgentPolicyState();
864
+ return;
865
+ }
866
+ }
867
+ const config = vscode.workspace.getConfiguration("flowseeker");
868
+ const target = vscode.ConfigurationTarget.Global;
869
+ const rules = (message.rulesText ?? "")
870
+ .split(/\r?\n/)
871
+ .map((rule) => rule.trim())
872
+ .filter(Boolean);
873
+ await config.update("agent.autoApprove.readWorkspace", Boolean(message.readWorkspace), target);
874
+ await config.update("agent.autoApprove.editWorkspace", Boolean(message.editWorkspace), target);
875
+ await config.update("agent.autoApprove.safeCommands", Boolean(message.safeCommands), target);
876
+ await config.update("agent.autoApprove.allCommands", Boolean(message.allCommands), target);
877
+ await config.update("agent.autoApprove.mcpTools", Boolean(message.mcpTools), target);
878
+ await config.update("agent.rules", rules, target);
879
+ this.postAgentPolicyState();
880
+ }
881
+ post(message) {
882
+ void this.view?.webview.postMessage(message);
883
+ }
884
+ postGatewayFeedback(status, text) {
885
+ this.post({ type: "gatewayFeedback", status, text });
886
+ }
887
+ postUsagePayload(usageSummary, profiles) {
888
+ const now = Date.now();
889
+ const todayStart = new Date();
890
+ todayStart.setHours(0, 0, 0, 0);
891
+ const tomorrow = new Date(todayStart);
892
+ tomorrow.setDate(tomorrow.getDate() + 1);
893
+ const nextMonth = new Date(todayStart.getFullYear(), todayStart.getMonth() + 1, 1);
894
+ const dailyResetMs = tomorrow.getTime();
895
+ const monthlyResetMs = nextMonth.getTime();
896
+ if (!usageSummary) {
897
+ // Return empty usage with just connected profiles
898
+ if (!profiles.length)
899
+ return undefined;
900
+ return {
901
+ todayCostUSD: 0,
902
+ weekCostUSD: 0,
903
+ totalCostUSD: 0,
904
+ totalRequests: 0,
905
+ models: profiles.map((p) => ({
906
+ provider: p.provider,
907
+ model: p.model,
908
+ dailyTokensUsed: -1,
909
+ dailyTokenLimit: -1,
910
+ dailyPct: -1,
911
+ quotaSource: "unknown",
912
+ monthlyCostUsed: 0,
913
+ monthlyCostLimit: 10,
914
+ monthlyPct: 0,
915
+ requestsToday: 0,
916
+ dailyResetMs,
917
+ monthlyResetMs
918
+ }))
919
+ };
920
+ }
921
+ // Merge usage data with all connected profiles
922
+ const usageByKey = new Map();
923
+ for (const m of usageSummary.models) {
924
+ usageByKey.set(`${m.provider}|${m.model}`, m);
925
+ }
926
+ const mergedModels = profiles.map((p) => {
927
+ const key = `${p.provider}|${p.model}`;
928
+ const existing = usageByKey.get(key);
929
+ if (existing)
930
+ return existing;
931
+ return {
932
+ provider: p.provider,
933
+ model: p.model,
934
+ dailyTokensUsed: -1,
935
+ dailyTokenLimit: -1,
936
+ dailyPct: -1,
937
+ quotaSource: "unknown",
938
+ monthlyCostUsed: 0,
939
+ monthlyCostLimit: 10,
940
+ monthlyPct: 0,
941
+ requestsToday: 0,
942
+ dailyResetMs,
943
+ monthlyResetMs
944
+ };
945
+ });
946
+ // Also include any models with usage that aren't in profiles
947
+ for (const m of usageSummary.models) {
948
+ const key = `${m.provider}|${m.model}`;
949
+ if (!profiles.some((p) => `${p.provider}|${p.model}` === key)) {
950
+ mergedModels.push(m);
951
+ }
952
+ }
953
+ return {
954
+ todayCostUSD: usageSummary.todayCostUSD,
955
+ weekCostUSD: usageSummary.thisWeekCostUSD,
956
+ totalCostUSD: usageSummary.totalCostUSD,
957
+ totalRequests: usageSummary.totalRequests,
958
+ models: mergedModels
959
+ };
960
+ }
961
+ async postChat(message, threadId = this.activeThreadId) {
962
+ const thread = this.threadById(threadId) ?? this.ensureActiveThread();
963
+ thread.messages.push(message);
964
+ if (thread.messages.length > maxHistoryMessages) {
965
+ thread.messages = thread.messages.slice(-maxHistoryMessages);
966
+ }
967
+ if (message.kind === "user" && (thread.title === "New chat" || thread.messages.filter((item) => item.kind === "user").length === 1)) {
968
+ thread.title = titleFromMessage(message.text);
969
+ }
970
+ thread.updatedAt = new Date().toISOString();
971
+ this.sortThreads();
972
+ await this.saveThreads();
973
+ if (thread.id === this.activeThreadId) {
974
+ this.post(renderHistoryMessage(message));
975
+ }
976
+ this.postThreadList();
977
+ }
978
+ async restoreThreads() {
979
+ const loaded = this.loadThreads();
980
+ this.threads = loaded.threads;
981
+ this.activeThreadId = loaded.activeThreadId;
982
+ await this.saveThreads();
983
+ this.postThreadState();
984
+ }
985
+ async createNewThread() {
986
+ const thread = createThread();
987
+ this.threads.unshift(thread);
988
+ this.trimThreads();
989
+ this.activeThreadId = thread.id;
990
+ await this.saveThreads();
991
+ this.postThreadState();
992
+ }
993
+ async selectThread(threadId) {
994
+ if (!this.threads.some((thread) => thread.id === threadId)) {
995
+ return;
996
+ }
997
+ this.activeThreadId = threadId;
998
+ await this.context.workspaceState.update(activeThreadStateKey, this.activeThreadId);
999
+ this.postThreadState();
1000
+ }
1001
+ async deleteThread(threadId) {
1002
+ this.threads = this.threads.filter((thread) => thread.id !== threadId);
1003
+ if (this.threads.length === 0) {
1004
+ this.threads = [createThread()];
1005
+ }
1006
+ if (this.activeThreadId === threadId || !this.threads.some((thread) => thread.id === this.activeThreadId)) {
1007
+ this.activeThreadId = this.threads[0].id;
1008
+ }
1009
+ await this.saveThreads();
1010
+ this.postThreadState();
1011
+ }
1012
+ loadThreads() {
1013
+ let threads = this.context.workspaceState.get(threadsStateKey, []);
1014
+ const legacyHistory = this.context.workspaceState.get(legacyHistoryStateKey, []);
1015
+ if (threads.length === 0 && legacyHistory.length > 0) {
1016
+ const now = new Date().toISOString();
1017
+ threads = [{
1018
+ id: createThreadId(),
1019
+ title: titleFromMessage(legacyHistory.find((message) => message.kind === "user")?.text ?? "Imported chat"),
1020
+ createdAt: now,
1021
+ updatedAt: now,
1022
+ messages: legacyHistory.slice(-maxHistoryMessages)
1023
+ }];
1024
+ }
1025
+ if (threads.length === 0) {
1026
+ threads = [createThread()];
1027
+ }
1028
+ threads = normalizeThreads(threads);
1029
+ const storedActive = this.context.workspaceState.get(activeThreadStateKey);
1030
+ const activeThreadId = storedActive && threads.some((thread) => thread.id === storedActive)
1031
+ ? storedActive
1032
+ : threads[0].id;
1033
+ return { threads, activeThreadId };
1034
+ }
1035
+ ensureActiveThread() {
1036
+ const existing = this.threads.find((thread) => thread.id === this.activeThreadId);
1037
+ if (existing) {
1038
+ return existing;
1039
+ }
1040
+ const thread = createThread();
1041
+ this.threads.unshift(thread);
1042
+ this.activeThreadId = thread.id;
1043
+ return thread;
1044
+ }
1045
+ activeThread() {
1046
+ return this.ensureActiveThread();
1047
+ }
1048
+ threadById(threadId) {
1049
+ return this.threads.find((thread) => thread.id === threadId);
1050
+ }
1051
+ async saveThreads() {
1052
+ this.sortThreads();
1053
+ this.trimThreads();
1054
+ await this.context.workspaceState.update(threadsStateKey, this.threads);
1055
+ await this.context.workspaceState.update(activeThreadStateKey, this.activeThreadId);
1056
+ }
1057
+ sortThreads() {
1058
+ this.threads = [...this.threads].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
1059
+ }
1060
+ trimThreads() {
1061
+ if (this.threads.length <= maxChatThreads) {
1062
+ return;
1063
+ }
1064
+ const active = this.threads.find((thread) => thread.id === this.activeThreadId);
1065
+ this.threads = this.threads.slice(0, maxChatThreads);
1066
+ if (active && !this.threads.some((thread) => thread.id === active.id)) {
1067
+ this.threads[this.threads.length - 1] = active;
1068
+ }
1069
+ }
1070
+ postThreadState() {
1071
+ const active = this.activeThread();
1072
+ this.post({
1073
+ type: "threadState",
1074
+ activeThreadId: active.id,
1075
+ threads: this.threadSummaries(),
1076
+ messages: active.messages.map(renderHistoryMessage)
1077
+ });
1078
+ }
1079
+ postThreadList() {
1080
+ this.post({
1081
+ type: "threads",
1082
+ activeThreadId: this.activeThreadId,
1083
+ threads: this.threadSummaries()
1084
+ });
1085
+ }
1086
+ threadSummaries() {
1087
+ return this.threads.map((thread) => ({
1088
+ id: thread.id,
1089
+ title: thread.title,
1090
+ updatedAt: thread.updatedAt,
1091
+ messageCount: thread.messages.length
1092
+ }));
1093
+ }
1094
+ findFeedbackSnapshot(runId) {
1095
+ for (const thread of this.threads) {
1096
+ const snapshot = [...thread.messages].reverse().find((message) => message.feedback?.id === runId)?.feedback;
1097
+ if (snapshot) {
1098
+ return snapshot;
1099
+ }
1100
+ }
1101
+ return undefined;
1102
+ }
1103
+ renderHtml(webview) {
1104
+ const nonce = getNonce();
1105
+ const logoUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, "resources", "flowseeker-logo-vector-v2.svg"));
1106
+ const providerOptions = aiProviders_1.AI_PROVIDER_DEFINITIONS.map((provider) => `<option value="${provider.id}">${escapeHtml(provider.label)}</option>`).join("");
1107
+ const providerMeta = JSON.stringify(Object.fromEntries(aiProviders_1.AI_PROVIDER_DEFINITIONS.map((provider) => [
1108
+ provider.id,
1109
+ {
1110
+ label: provider.label,
1111
+ defaultBaseUrl: provider.defaultBaseUrl,
1112
+ defaultModel: provider.defaultModel,
1113
+ requiresApiKey: provider.requiresApiKey,
1114
+ apiKeyLabel: provider.apiKeyLabel,
1115
+ apiKeyHint: provider.apiKeyHint ?? "",
1116
+ authMethod: provider.authMethod,
1117
+ oauthConfig: provider.oauthConfig ?? null,
1118
+ supportsModelList: provider.supportsModelList,
1119
+ modelLabel: provider.id === "codexCli" || provider.id === "claudeCode" ? "Model / args" : "Model",
1120
+ baseUrlLabel: provider.id === "codexCli" || provider.id === "claudeCode" ? "Command" : "Base URL",
1121
+ modelPlaceholder: provider.id === "customGateway" ? "deepseek-v4-pro[1m]" : provider.id === "customAnthropic" ? "claude-sonnet-4-5" : provider.id === "codexCli" || provider.id === "claudeCode" ? "default or model name" : provider.defaultModel,
1122
+ baseUrlPlaceholder: provider.id === "customGateway" ? "https://api.example.com/v1" : provider.id === "customAnthropic" ? "https://api.example.com/v1" : provider.defaultBaseUrl
1123
+ }
1124
+ ])));
1125
+ const csp = [
1126
+ "default-src 'none'",
1127
+ `img-src ${webview.cspSource}`,
1128
+ `style-src ${webview.cspSource} 'unsafe-inline'`,
1129
+ `script-src 'nonce-${nonce}'`
1130
+ ].join("; ");
1131
+ return `<!DOCTYPE html>
1132
+ <html lang="en">
1133
+ <head>
1134
+ <meta charset="UTF-8">
1135
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
1136
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1137
+ <title>FlowSeeker Chat</title>
1138
+ <style>
1139
+ :root {
1140
+ color-scheme: light dark;
1141
+ --border: var(--vscode-widget-border, color-mix(in srgb, var(--vscode-foreground) 17%, transparent));
1142
+ --muted: var(--vscode-descriptionForeground);
1143
+ --bg: var(--vscode-sideBar-background);
1144
+ --panel: var(--vscode-editor-background);
1145
+ --panel-alt: var(--vscode-editorWidget-background, var(--vscode-sideBar-background));
1146
+ --input: var(--vscode-input-background);
1147
+ --input-border: var(--vscode-input-border, var(--vscode-widget-border));
1148
+ --button: color-mix(in srgb, var(--vscode-foreground) 14%, var(--panel-alt));
1149
+ --button-hover: color-mix(in srgb, var(--vscode-foreground) 20%, var(--panel-alt));
1150
+ --button-fg: var(--vscode-foreground);
1151
+ --surface: color-mix(in srgb, var(--panel) 44%, var(--bg));
1152
+ --surface-raised: color-mix(in srgb, var(--panel-alt) 72%, var(--bg));
1153
+ --surface-hover: color-mix(in srgb, var(--vscode-foreground) 7%, transparent);
1154
+ --hairline: color-mix(in srgb, var(--border) 68%, transparent);
1155
+ --secondary-button: var(--vscode-button-secondaryBackground);
1156
+ --secondary-button-fg: var(--vscode-button-secondaryForeground);
1157
+ --focus: var(--vscode-focusBorder);
1158
+ --danger: var(--vscode-errorForeground);
1159
+ --link: var(--vscode-textLink-foreground);
1160
+ --success: var(--vscode-testing-iconPassed, #2ea043);
1161
+ --success-bg: color-mix(in srgb, var(--success) 10%, var(--bg));
1162
+ --success-border: color-mix(in srgb, var(--success) 36%, transparent);
1163
+ --row-hover: var(--vscode-list-hoverBackground, color-mix(in srgb, var(--vscode-foreground) 7%, transparent));
1164
+ --row-active: color-mix(in srgb, var(--vscode-foreground) 12%, transparent);
1165
+ --toggle-active: color-mix(in srgb, var(--vscode-foreground) 16%, var(--panel-alt));
1166
+ --accent-bg: color-mix(in srgb, var(--vscode-foreground) 6%, transparent);
1167
+ --assistant-bg: transparent;
1168
+ --user-bg: color-mix(in srgb, var(--vscode-foreground) 8%, var(--panel));
1169
+ --font: var(--vscode-font-family, system-ui, -apple-system, sans-serif);
1170
+ --mono: var(--vscode-editor-font-family, 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace);
1171
+ }
1172
+ * { box-sizing: border-box; }
1173
+ body {
1174
+ margin: 0;
1175
+ background: color-mix(in srgb, var(--bg) 74%, var(--panel));
1176
+ color: var(--vscode-foreground);
1177
+ font-family: var(--font);
1178
+ font-size: 13px;
1179
+ line-height: 1.5;
1180
+ height: 100vh;
1181
+ min-width: 300px;
1182
+ display: flex;
1183
+ flex-direction: column;
1184
+ overflow: hidden;
1185
+ }
1186
+
1187
+ /* Header */
1188
+ .header {
1189
+ display: flex; align-items: center; gap: 8px;
1190
+ min-height: 40px;
1191
+ padding: 7px 10px;
1192
+ border-bottom: 1px solid var(--hairline);
1193
+ background: color-mix(in srgb, var(--panel) 62%, var(--bg));
1194
+ flex-shrink: 0;
1195
+ }
1196
+ .brand {
1197
+ width: 24px; height: 24px;
1198
+ border-radius: 6px;
1199
+ display: flex; align-items: center; justify-content: center;
1200
+ background: var(--surface-raised);
1201
+ border: 1px solid var(--hairline);
1202
+ flex-shrink: 0;
1203
+ overflow: hidden;
1204
+ }
1205
+ .brand img { width: 18px; height: 18px; display: block; }
1206
+ .wordmark {
1207
+ font-family: var(--mono);
1208
+ font-size: 14px;
1209
+ font-weight: 1000;
1210
+ line-height: .95;
1211
+ letter-spacing: .045em;
1212
+ text-transform: uppercase;
1213
+ transform: scaleX(1.08) scaleY(1.2);
1214
+ transform-origin: left center;
1215
+ position: relative;
1216
+ display: inline-block;
1217
+ color: var(--vscode-foreground);
1218
+ -webkit-text-fill-color: var(--vscode-foreground);
1219
+ -webkit-text-stroke: .45px color-mix(in srgb, var(--vscode-foreground) 62%, transparent);
1220
+ text-shadow:
1221
+ 1px 0 0 #ff004c,
1222
+ -1px 0 0 #00e5ff,
1223
+ 0 1px 0 #7cff00,
1224
+ 0 0 8px color-mix(in srgb, #00e5ff 44%, transparent),
1225
+ 0 0 18px color-mix(in srgb, #ff004c 32%, transparent);
1226
+ image-rendering: pixelated;
1227
+ animation: wordmarkRgbPulse 2.8s ease-in-out infinite;
1228
+ }
1229
+ .wordmark::before,
1230
+ .wordmark::after {
1231
+ content: attr(data-text);
1232
+ position: absolute;
1233
+ inset: 0;
1234
+ pointer-events: none;
1235
+ -webkit-text-fill-color: transparent;
1236
+ color: transparent;
1237
+ mix-blend-mode: screen;
1238
+ opacity: .9;
1239
+ }
1240
+ .wordmark::before {
1241
+ -webkit-text-stroke: 1px #ff004c;
1242
+ text-shadow: 0 0 8px #ff004c, 0 0 18px #ff004c;
1243
+ animation: wordmarkRgbRed 1.8s steps(2, end) infinite;
1244
+ }
1245
+ .wordmark::after {
1246
+ -webkit-text-stroke: 1px #00e5ff;
1247
+ text-shadow: 0 0 8px #00e5ff, 0 0 18px #00e5ff;
1248
+ animation: wordmarkRgbCyan 1.8s steps(2, end) infinite;
1249
+ }
1250
+ @keyframes wordmarkRgbPulse {
1251
+ 0%, 100% {
1252
+ text-shadow:
1253
+ 1px 0 0 #ff004c,
1254
+ -1px 0 0 #00e5ff,
1255
+ 0 1px 0 #7cff00,
1256
+ 0 0 8px color-mix(in srgb, #00e5ff 44%, transparent),
1257
+ 0 0 18px color-mix(in srgb, #ff004c 32%, transparent);
1258
+ }
1259
+ 50% {
1260
+ text-shadow:
1261
+ 2px 0 0 #ff004c,
1262
+ -2px 0 0 #00e5ff,
1263
+ 0 1px 0 #7cff00,
1264
+ 0 0 12px color-mix(in srgb, #7cff00 42%, transparent),
1265
+ 0 0 24px color-mix(in srgb, #00e5ff 34%, transparent);
1266
+ }
1267
+ }
1268
+ @keyframes wordmarkRgbRed {
1269
+ 0%, 100% { transform: translate(-1px, 0); opacity: .55; }
1270
+ 50% { transform: translate(1px, -1px); opacity: .95; }
1271
+ }
1272
+ @keyframes wordmarkRgbCyan {
1273
+ 0%, 100% { transform: translate(1px, 0); opacity: .55; }
1274
+ 50% { transform: translate(-1px, 1px); opacity: .95; }
1275
+ }
1276
+ .title { font-weight: 650; font-size: 13px; letter-spacing: 0; }
1277
+ .spacer { flex: 1; }
1278
+ .header button {
1279
+ width: 26px; height: 26px;
1280
+ border: 0; border-radius: 6px;
1281
+ background: transparent;
1282
+ color: var(--muted);
1283
+ cursor: pointer;
1284
+ display: flex; align-items: center; justify-content: center;
1285
+ font-size: 14px;
1286
+ transition: background .15s, color .15s;
1287
+ }
1288
+ .header button:hover { background: var(--surface-hover); color: var(--vscode-foreground); }
1289
+
1290
+ .status-line {
1291
+ display: flex;
1292
+ align-items: center;
1293
+ gap: 7px;
1294
+ padding: 5px 10px;
1295
+ font-size: 11px; color: var(--muted);
1296
+ border-bottom: 1px solid var(--hairline);
1297
+ background: color-mix(in srgb, var(--panel) 28%, var(--bg));
1298
+ flex-shrink: 0;
1299
+ overflow: hidden;
1300
+ }
1301
+ .status-badge {
1302
+ font-size: 9px;
1303
+ font-weight: 700;
1304
+ text-transform: uppercase;
1305
+ letter-spacing: 0.5px;
1306
+ padding: 1px 6px;
1307
+ border-radius: 4px;
1308
+ background: color-mix(in srgb, var(--muted) 25%, transparent);
1309
+ color: var(--muted);
1310
+ flex: 0 0 auto;
1311
+ }
1312
+ .status-line.connected .status-badge {
1313
+ background: color-mix(in srgb, var(--vscode-terminal-ansiGreen) 25%, transparent);
1314
+ color: var(--vscode-terminal-ansiGreen);
1315
+ }
1316
+ .status-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1317
+
1318
+ .quota-panel {
1319
+ display: none;
1320
+ border-bottom: 1px solid var(--hairline);
1321
+ background: var(--surface);
1322
+ flex-shrink: 0;
1323
+ }
1324
+ .quota-panel.open { display: block; }
1325
+ .quota-header {
1326
+ display: flex; align-items: center; gap: 6px;
1327
+ padding: 6px 10px;
1328
+ cursor: pointer;
1329
+ font-size: 11px; color: var(--muted);
1330
+ user-select: none;
1331
+ }
1332
+ .quota-header:hover { color: var(--vscode-foreground); }
1333
+ .quota-header .chevron { font-size: 9px; transition: transform .15s; }
1334
+ .quota-header .chevron.open { transform: rotate(90deg); }
1335
+ .quota-summary-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1336
+ .quota-badge {
1337
+ padding: 1px 7px; border-radius: 999px;
1338
+ font-size: 10px; font-weight: 700;
1339
+ }
1340
+ .quota-badge.ok { background: color-mix(in srgb, var(--vscode-foreground) 10%, transparent); color: var(--vscode-foreground); }
1341
+ .quota-badge.warn { background: color-mix(in srgb, #d29922 15%, transparent); color: #d29922; }
1342
+ .quota-badge.danger { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); }
1343
+ .quota-detail {
1344
+ display: none;
1345
+ padding: 0 10px 8px;
1346
+ }
1347
+ .quota-detail.open { display: block; }
1348
+ .quota-row {
1349
+ display: grid;
1350
+ grid-template-columns: minmax(0, 1fr) auto;
1351
+ gap: 4px 10px;
1352
+ align-items: center;
1353
+ padding: 4px 0;
1354
+ font-size: 11px;
1355
+ border-bottom: 1px solid color-mix(in srgb, var(--hairline) 50%, transparent);
1356
+ }
1357
+ .quota-row:last-child { border-bottom: 0; }
1358
+ .quota-model-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1359
+ .quota-stats { color: var(--muted); font-size: 10px; grid-column: 1 / -1; }
1360
+ .quota-bar-wrap {
1361
+ grid-column: 1 / -1;
1362
+ height: 4px;
1363
+ border-radius: 2px;
1364
+ background: color-mix(in srgb, var(--vscode-foreground) 8%, transparent);
1365
+ overflow: hidden;
1366
+ }
1367
+ .quota-bar-fill {
1368
+ height: 100%;
1369
+ border-radius: 2px;
1370
+ transition: width .4s ease;
1371
+ }
1372
+ .quota-bar-fill.low { background: var(--vscode-foreground); opacity: .6; }
1373
+ .quota-bar-fill.mid { background: #d29922; }
1374
+ .quota-bar-fill.high { background: var(--danger); }
1375
+
1376
+ .agent-policy {
1377
+ display: grid;
1378
+ grid-template-columns: auto minmax(0, 1fr);
1379
+ grid-template-areas:
1380
+ "main main"
1381
+ "actions quota";
1382
+ align-items: center;
1383
+ gap: 5px 8px;
1384
+ padding: 6px 10px;
1385
+ border-top: 1px solid var(--hairline);
1386
+ background: color-mix(in srgb, var(--panel) 38%, var(--bg));
1387
+ color: var(--muted);
1388
+ font-size: 11px;
1389
+ flex-shrink: 0;
1390
+ }
1391
+ .agent-policy-main {
1392
+ grid-area: main;
1393
+ display: flex;
1394
+ align-items: center;
1395
+ gap: 6px;
1396
+ min-width: 0;
1397
+ }
1398
+ .agent-policy-actions {
1399
+ grid-area: actions;
1400
+ display: grid;
1401
+ grid-template-columns: 1fr 1fr;
1402
+ gap: 6px;
1403
+ width: 132px;
1404
+ }
1405
+ .agent-policy-label {
1406
+ color: var(--vscode-foreground);
1407
+ font-weight: 700;
1408
+ flex: 0 0 auto;
1409
+ }
1410
+ .agent-policy-text {
1411
+ overflow: hidden;
1412
+ text-overflow: ellipsis;
1413
+ white-space: nowrap;
1414
+ min-width: 0;
1415
+ }
1416
+ .agent-policy button {
1417
+ width: 100%;
1418
+ border: 0;
1419
+ border-radius: 5px;
1420
+ background: transparent;
1421
+ color: var(--muted);
1422
+ cursor: pointer;
1423
+ font-size: 11px;
1424
+ padding: 3px 6px;
1425
+ text-align: center;
1426
+ }
1427
+ .agent-policy button:hover {
1428
+ background: var(--surface-hover);
1429
+ color: var(--vscode-foreground);
1430
+ }
1431
+ .agent-policy .quota-panel {
1432
+ grid-area: quota;
1433
+ display: block;
1434
+ min-width: 0;
1435
+ border: 0;
1436
+ background: transparent;
1437
+ }
1438
+ .agent-policy .quota-panel:not(.open) {
1439
+ display: block;
1440
+ }
1441
+ .agent-policy .quota-header {
1442
+ min-height: 23px;
1443
+ padding: 2px 0;
1444
+ }
1445
+ .agent-policy .quota-detail {
1446
+ grid-column: 1 / -1;
1447
+ padding: 4px 0 0;
1448
+ }
1449
+ .agent-policy.relaxed .agent-policy-label { color: var(--success); }
1450
+ .agent-policy.risky .agent-policy-label { color: var(--vscode-testing-iconQueued, #d29922); }
1451
+
1452
+
1453
+ .thread-panel {
1454
+ display: none;
1455
+ border-bottom: 1px solid var(--hairline);
1456
+ background: var(--surface);
1457
+ max-height: 300px;
1458
+ overflow-y: auto;
1459
+ flex-shrink: 0;
1460
+ }
1461
+ .thread-panel.open { display: block; }
1462
+ .thread-panel-head {
1463
+ display: flex;
1464
+ align-items: center;
1465
+ gap: 8px;
1466
+ padding: 8px 10px 4px;
1467
+ color: var(--muted);
1468
+ font-size: 11px;
1469
+ font-weight: 650;
1470
+ text-transform: uppercase;
1471
+ letter-spacing: .04em;
1472
+ }
1473
+ .thread-list {
1474
+ display: flex;
1475
+ flex-direction: column;
1476
+ gap: 2px;
1477
+ padding: 0 8px 8px;
1478
+ }
1479
+ .thread-row {
1480
+ display: grid;
1481
+ grid-template-columns: 1fr auto;
1482
+ gap: 8px;
1483
+ align-items: center;
1484
+ padding: 8px;
1485
+ border-radius: 6px;
1486
+ cursor: pointer;
1487
+ color: var(--vscode-foreground);
1488
+ }
1489
+ .thread-row:hover { background: var(--surface-hover); }
1490
+ .thread-row.active { background: var(--row-active); }
1491
+ .thread-main { min-width: 0; }
1492
+ .thread-title {
1493
+ overflow: hidden;
1494
+ text-overflow: ellipsis;
1495
+ white-space: nowrap;
1496
+ font-size: 12px;
1497
+ font-weight: 600;
1498
+ }
1499
+ .thread-meta {
1500
+ color: var(--muted);
1501
+ font-size: 10px;
1502
+ margin-top: 1px;
1503
+ }
1504
+ .thread-delete {
1505
+ width: 22px;
1506
+ height: 22px;
1507
+ padding: 0;
1508
+ border-radius: 5px;
1509
+ opacity: .55;
1510
+ color: var(--muted);
1511
+ background: transparent;
1512
+ }
1513
+ .thread-row:hover .thread-delete { opacity: 1; }
1514
+ .thread-delete:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
1515
+
1516
+ .login-panel {
1517
+ display: none;
1518
+ border-bottom: 1px solid var(--hairline);
1519
+ padding: 0 10px 10px;
1520
+ background: var(--bg);
1521
+ gap: 8px;
1522
+ overflow-y: auto;
1523
+ max-height: min(58vh, 520px);
1524
+ flex-shrink: 0;
1525
+ }
1526
+ .login-panel.open { display: grid; }
1527
+ .panel-head {
1528
+ display: grid;
1529
+ grid-template-columns: minmax(0, 1fr) auto;
1530
+ align-items: center;
1531
+ gap: 8px;
1532
+ margin: 0 -10px 2px;
1533
+ padding: 8px 10px;
1534
+ border-bottom: 1px solid var(--hairline);
1535
+ background: color-mix(in srgb, var(--panel) 38%, var(--bg));
1536
+ }
1537
+ .panel-head-main { min-width: 0; }
1538
+ .panel-head-title {
1539
+ color: var(--vscode-foreground);
1540
+ font-size: 11px;
1541
+ font-weight: 800;
1542
+ text-transform: uppercase;
1543
+ letter-spacing: .04em;
1544
+ }
1545
+ .panel-head-subtitle {
1546
+ margin-top: 1px;
1547
+ color: var(--muted);
1548
+ font-size: 10px;
1549
+ overflow: hidden;
1550
+ text-overflow: ellipsis;
1551
+ white-space: nowrap;
1552
+ }
1553
+ .panel-close {
1554
+ width: 24px;
1555
+ height: 24px;
1556
+ padding: 0;
1557
+ border-radius: 6px;
1558
+ background: transparent;
1559
+ color: var(--muted);
1560
+ }
1561
+ .panel-close:hover { background: var(--surface-hover); color: var(--vscode-foreground); }
1562
+ .login-panel label {
1563
+ display: grid;
1564
+ gap: 3px;
1565
+ color: var(--muted);
1566
+ font-size: 11px;
1567
+ font-weight: 600;
1568
+ }
1569
+ .login-panel input,
1570
+ .login-panel select {
1571
+ border: 1px solid var(--hairline);
1572
+ background: var(--input);
1573
+ color: var(--vscode-input-foreground);
1574
+ border-radius: 7px; padding: 6px 8px;
1575
+ font-size: 12px; font-family: var(--font);
1576
+ min-width: 0;
1577
+ }
1578
+ .login-row {
1579
+ display: flex;
1580
+ flex-wrap: wrap;
1581
+ gap: 6px;
1582
+ }
1583
+ .login-row button {
1584
+ flex: 1 1 110px;
1585
+ min-width: 0;
1586
+ }
1587
+ .key-saved { color: var(--vscode-terminal-ansiGreen); }
1588
+ .key-saved::placeholder { color: var(--vscode-terminal-ansiGreen); opacity: 0.7; }
1589
+ .agent-control-panel {
1590
+ display: grid;
1591
+ gap: 7px;
1592
+ margin-top: 4px;
1593
+ padding-top: 9px;
1594
+ border-top: 1px solid var(--hairline);
1595
+ }
1596
+ #agentControls {
1597
+ display: none;
1598
+ border-bottom: 1px solid var(--hairline);
1599
+ padding: 0 10px 10px;
1600
+ background: var(--bg);
1601
+ gap: 8px;
1602
+ overflow-y: auto;
1603
+ max-height: min(58vh, 520px);
1604
+ flex-shrink: 0;
1605
+ }
1606
+ #agentControls.open { display: grid; }
1607
+ .panel-section-title {
1608
+ color: var(--vscode-foreground);
1609
+ font-size: 11px;
1610
+ font-weight: 800;
1611
+ text-transform: uppercase;
1612
+ letter-spacing: .04em;
1613
+ }
1614
+ .toggle-row {
1615
+ display: grid !important;
1616
+ grid-template-columns: auto minmax(0, 1fr);
1617
+ align-items: start;
1618
+ gap: 8px !important;
1619
+ color: var(--vscode-foreground) !important;
1620
+ font-size: 12px !important;
1621
+ font-weight: 500 !important;
1622
+ line-height: 1.35;
1623
+ }
1624
+ .toggle-row input {
1625
+ width: 14px;
1626
+ height: 14px;
1627
+ margin: 1px 0 0;
1628
+ accent-color: var(--vscode-foreground);
1629
+ }
1630
+ .toggle-row strong {
1631
+ display: block;
1632
+ font-size: 12px;
1633
+ font-weight: 650;
1634
+ }
1635
+ .toggle-row span span {
1636
+ display: block;
1637
+ margin-top: 1px;
1638
+ color: var(--muted);
1639
+ font-size: 10px;
1640
+ }
1641
+ .agent-rules {
1642
+ display: grid;
1643
+ gap: 4px;
1644
+ }
1645
+ .agent-rules textarea {
1646
+ min-height: 68px;
1647
+ max-height: 160px;
1648
+ resize: vertical;
1649
+ border: 1px solid var(--hairline);
1650
+ background: var(--input);
1651
+ color: var(--vscode-input-foreground);
1652
+ border-radius: 7px;
1653
+ padding: 7px 8px;
1654
+ font-size: 12px;
1655
+ font-family: var(--font);
1656
+ line-height: 1.4;
1657
+ outline: none;
1658
+ }
1659
+ .agent-rules textarea:focus {
1660
+ border-color: color-mix(in srgb, var(--vscode-foreground) 28%, var(--hairline));
1661
+ }
1662
+ .gateway-status {
1663
+ display: grid;
1664
+ grid-template-columns: auto minmax(0, 1fr);
1665
+ align-items: center;
1666
+ gap: 8px;
1667
+ border: 1px solid var(--hairline);
1668
+ border-radius: 7px;
1669
+ background: color-mix(in srgb, var(--panel) 46%, transparent);
1670
+ padding: 8px;
1671
+ color: var(--muted);
1672
+ font-size: 11px;
1673
+ }
1674
+ .gateway-dot {
1675
+ width: 9px;
1676
+ height: 9px;
1677
+ border-radius: 999px;
1678
+ background: color-mix(in srgb, var(--muted) 55%, transparent);
1679
+ }
1680
+ .gateway-status.connected .gateway-dot { background: var(--success); }
1681
+ .gateway-status.testing .gateway-dot {
1682
+ background: var(--vscode-testing-iconQueued, #d29922);
1683
+ animation: pulseDot 1s ease-in-out infinite;
1684
+ }
1685
+ @keyframes pulseDot { 50% { opacity: .35; transform: scale(.8); } }
1686
+ .gateway-status-text {
1687
+ overflow: hidden;
1688
+ text-overflow: ellipsis;
1689
+ white-space: nowrap;
1690
+ }
1691
+ .gateway-feedback {
1692
+ display: none;
1693
+ border: 1px solid var(--hairline);
1694
+ border-radius: 7px;
1695
+ padding: 8px;
1696
+ font-size: 11px;
1697
+ line-height: 1.35;
1698
+ }
1699
+ .gateway-feedback.show { display: block; }
1700
+ .gateway-feedback.success {
1701
+ color: var(--success);
1702
+ border-color: var(--success-border);
1703
+ background: var(--success-bg);
1704
+ }
1705
+ .gateway-feedback.error {
1706
+ color: var(--danger);
1707
+ border-color: color-mix(in srgb, var(--danger) 35%, transparent);
1708
+ background: color-mix(in srgb, var(--danger) 8%, var(--bg));
1709
+ }
1710
+ .gateway-feedback.warning {
1711
+ color: var(--vscode-testing-iconQueued, #d29922);
1712
+ border-color: color-mix(in srgb, var(--vscode-testing-iconQueued, #d29922) 35%, transparent);
1713
+ background: color-mix(in srgb, var(--vscode-testing-iconQueued, #d29922) 10%, var(--bg));
1714
+ }
1715
+ .gateway-feedback.info {
1716
+ color: var(--muted);
1717
+ background: color-mix(in srgb, var(--panel) 46%, transparent);
1718
+ }
1719
+ .profile-list {
1720
+ display: flex;
1721
+ flex-direction: column;
1722
+ gap: 4px;
1723
+ margin-bottom: 4px;
1724
+ }
1725
+ .profile-empty {
1726
+ color: var(--muted);
1727
+ font-size: 11px;
1728
+ padding: 6px 2px 2px;
1729
+ }
1730
+ .profile-row {
1731
+ display: grid;
1732
+ grid-template-columns: 1fr auto;
1733
+ gap: 8px;
1734
+ align-items: center;
1735
+ border: 1px solid var(--hairline);
1736
+ background: color-mix(in srgb, var(--panel) 46%, transparent);
1737
+ border-radius: 7px;
1738
+ padding: 8px;
1739
+ cursor: pointer;
1740
+ }
1741
+ .profile-row:hover { background: var(--surface-hover); }
1742
+ .profile-row.active {
1743
+ border-color: color-mix(in srgb, var(--vscode-foreground) 20%, var(--hairline));
1744
+ background: color-mix(in srgb, var(--vscode-foreground) 8%, var(--panel-alt));
1745
+ }
1746
+ .profile-main { min-width: 0; }
1747
+ .profile-name {
1748
+ font-size: 12px;
1749
+ font-weight: 700;
1750
+ overflow: hidden;
1751
+ text-overflow: ellipsis;
1752
+ white-space: nowrap;
1753
+ }
1754
+ .profile-meta {
1755
+ margin-top: 1px;
1756
+ color: var(--muted);
1757
+ font-size: 10px;
1758
+ overflow: hidden;
1759
+ text-overflow: ellipsis;
1760
+ white-space: nowrap;
1761
+ font-family: var(--mono);
1762
+ }
1763
+ .profile-actions {
1764
+ display: flex;
1765
+ gap: 4px;
1766
+ align-items: center;
1767
+ }
1768
+ .profile-state {
1769
+ width: 8px;
1770
+ height: 8px;
1771
+ border-radius: 50%;
1772
+ background: color-mix(in srgb, var(--muted) 55%, transparent);
1773
+ }
1774
+ .profile-row.connected .profile-state { background: var(--success); }
1775
+
1776
+ /* Messages area */
1777
+ .messages {
1778
+ flex: 1; overflow-y: auto;
1779
+ padding: 10px 10px 14px;
1780
+ display: flex; flex-direction: column; gap: 4px;
1781
+ background: color-mix(in srgb, var(--bg) 70%, var(--panel));
1782
+ }
1783
+ .messages::-webkit-scrollbar { width: 6px; }
1784
+ .messages::-webkit-scrollbar-thumb {
1785
+ background: color-mix(in srgb, var(--muted) 35%, transparent);
1786
+ border-radius: 3px;
1787
+ }
1788
+
1789
+ .empty-state {
1790
+ margin: auto;
1791
+ text-align: center;
1792
+ color: var(--muted);
1793
+ padding: 24px 16px;
1794
+ }
1795
+ .empty-state .brand-lg {
1796
+ display: none;
1797
+ }
1798
+ .brand-lg img { width: 32px; height: 32px; display: block; }
1799
+ .empty-title .wordmark {
1800
+ display: inline-block;
1801
+ font-size: clamp(30px, 12vw, 46px);
1802
+ letter-spacing: .035em;
1803
+ transform: scaleX(1.12) scaleY(1.24);
1804
+ margin-bottom: 10px;
1805
+ }
1806
+ .empty-title { margin-bottom: 12px; }
1807
+ .empty-title { font-weight: 650; font-size: 15px; color: var(--vscode-foreground); }
1808
+ .empty-hint { font-size: 12px; line-height: 1.6; }
1809
+
1810
+ /* Message row */
1811
+ .msg-row {
1812
+ display: flex;
1813
+ gap: 9px;
1814
+ padding: 8px 2px;
1815
+ animation: fadeIn .16s ease;
1816
+ }
1817
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(3px); } }
1818
+ .msg-row.user { flex-direction: row-reverse; }
1819
+ .msg-avatar {
1820
+ width: 24px; height: 24px;
1821
+ border-radius: 7px;
1822
+ display: flex; align-items: center; justify-content: center;
1823
+ flex-shrink: 0;
1824
+ font-weight: 650; font-size: 10px;
1825
+ border: 1px solid var(--hairline);
1826
+ user-select: none;
1827
+ overflow: hidden;
1828
+ }
1829
+ .msg-avatar img { width: 17px; height: 17px; display: block; }
1830
+ .msg-row.assistant .msg-avatar,
1831
+ .msg-row.status .msg-avatar,
1832
+ .msg-row.retrieval .msg-avatar {
1833
+ background: var(--surface-raised);
1834
+ color: var(--vscode-foreground);
1835
+ border-color: var(--hairline);
1836
+ }
1837
+ .msg-row.user .msg-avatar {
1838
+ background: color-mix(in srgb, var(--vscode-foreground) 9%, var(--surface-raised));
1839
+ color: var(--vscode-foreground);
1840
+ border-color: var(--hairline);
1841
+ }
1842
+ .msg-row.error .msg-avatar {
1843
+ color: var(--danger); border-color: var(--danger);
1844
+ background: color-mix(in srgb, var(--danger) 10%, var(--bg));
1845
+ }
1846
+
1847
+ .msg-body { flex: 1; min-width: 0; max-width: calc(100% - 33px); }
1848
+ .msg-row.user .msg-body { display: flex; flex-direction: column; align-items: flex-end; }
1849
+ .msg-label {
1850
+ color: var(--muted);
1851
+ font-size: 10px;
1852
+ font-weight: 650;
1853
+ line-height: 1.2;
1854
+ margin: 0 0 4px;
1855
+ text-transform: uppercase;
1856
+ letter-spacing: .04em;
1857
+ }
1858
+
1859
+ /* Chat bubbles */
1860
+ .bubble {
1861
+ border-radius: 8px;
1862
+ padding: 8px 10px;
1863
+ line-height: 1.55;
1864
+ overflow-wrap: anywhere;
1865
+ position: relative;
1866
+ }
1867
+ .msg-row.assistant .bubble,
1868
+ .msg-row.retrieval .bubble {
1869
+ background: var(--assistant-bg);
1870
+ border: 0;
1871
+ padding: 0 2px;
1872
+ border-radius: 0;
1873
+ }
1874
+ .msg-row.user .bubble {
1875
+ background: var(--user-bg);
1876
+ border: 1px solid var(--hairline);
1877
+ max-width: 92%;
1878
+ }
1879
+ .msg-row.status .bubble {
1880
+ color: var(--muted); font-size: 12px;
1881
+ background: transparent; border: 0;
1882
+ padding: 0 2px;
1883
+ }
1884
+ .msg-row.error .bubble {
1885
+ background: color-mix(in srgb, var(--danger) 8%, var(--bg));
1886
+ border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
1887
+ }
1888
+
1889
+ /* Markdown content */
1890
+ .content h1, .content h2, .content h3 { font-size: 1.05em; margin: 12px 0 4px; line-height: 1.3; }
1891
+ .content h4, .content h5, .content h6 { font-size: 1em; margin: 8px 0 2px; }
1892
+ .content p { margin: 0 0 8px; }
1893
+ .content p:last-child { margin-bottom: 0; }
1894
+ .content ul, .content ol { margin: 0 0 8px; padding-left: 20px; }
1895
+ .content li { margin-bottom: 2px; }
1896
+ .content strong { font-weight: 700; }
1897
+ .content em { font-style: italic; }
1898
+ .content a { color: var(--link); text-decoration: none; }
1899
+ .content a:hover { text-decoration: underline; }
1900
+ .content blockquote {
1901
+ border-left: 2px solid var(--border);
1902
+ padding: 2px 0 2px 12px; margin: 8px 0;
1903
+ color: var(--muted);
1904
+ }
1905
+ .content hr { border: 0; border-top: 1px solid var(--border); margin: 12px 0; }
1906
+ .content table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 12px; }
1907
+ .content th, .content td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
1908
+ .content th { background: color-mix(in srgb, var(--panel-alt) 50%, var(--bg)); }
1909
+ .content code:not(.hljs) {
1910
+ background: color-mix(in srgb, var(--input) 80%, var(--bg));
1911
+ border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
1912
+ border-radius: 4px; padding: 1px 5px;
1913
+ font-family: var(--mono); font-size: .9em;
1914
+ }
1915
+
1916
+ /* Code blocks */
1917
+ .code-block {
1918
+ margin: 10px 0;
1919
+ border: 1px solid var(--border);
1920
+ border-radius: 10px;
1921
+ overflow: hidden;
1922
+ background: color-mix(in srgb, #000 5%, var(--panel));
1923
+ }
1924
+ .code-header {
1925
+ display: flex; align-items: center;
1926
+ padding: 5px 8px 4px 14px;
1927
+ background: color-mix(in srgb, var(--panel-alt) 60%, var(--bg));
1928
+ border-bottom: 1px solid var(--border);
1929
+ font-size: 11px; color: var(--muted);
1930
+ font-family: var(--mono);
1931
+ }
1932
+ .code-header .lang { flex: 1; }
1933
+ .code-header button {
1934
+ border: 0; border-radius: 4px;
1935
+ background: transparent; color: var(--muted);
1936
+ cursor: pointer; padding: 3px 10px;
1937
+ font-size: 11px; font-family: var(--font);
1938
+ transition: background .15s, color .15s;
1939
+ }
1940
+ .code-header button:hover { background: var(--row-hover); color: var(--vscode-foreground); }
1941
+ .code-header button.copied { color: var(--success); }
1942
+ .code-block pre {
1943
+ margin: 0; padding: 12px 14px;
1944
+ overflow-x: auto;
1945
+ font-family: var(--mono); font-size: 12px; line-height: 1.5;
1946
+ }
1947
+ .code-block pre code { font-family: var(--mono); }
1948
+
1949
+ /* Syntax highlighting */
1950
+ .hljs-keyword { color: var(--vscode-symbolIcon-keywordForeground, #569cd6); }
1951
+ .hljs-string { color: var(--vscode-symbolIcon-stringForeground, #ce9178); }
1952
+ .hljs-number { color: var(--vscode-symbolIcon-numberForeground, #b5cea8); }
1953
+ .hljs-comment { color: var(--vscode-symbolIcon-commentForeground, #6a9955); font-style: italic; }
1954
+ .hljs-function { color: var(--vscode-symbolIcon-functionForeground, #dcdcaa); }
1955
+ .hljs-type { color: var(--vscode-symbolIcon-classForeground, #4ec9b0); }
1956
+ .hljs-variable { color: var(--vscode-symbolIcon-variableForeground, #9cdcfe); }
1957
+ .hljs-operator { color: var(--vscode-symbolIcon-operatorForeground, #d4d4d4); }
1958
+ .hljs-built_in { color: var(--vscode-symbolIcon-classForeground, #4ec9b0); }
1959
+ .hljs-attr { color: var(--vscode-symbolIcon-variableForeground, #9cdcfe); }
1960
+ .hljs-literal { color: var(--vscode-symbolIcon-constantForeground, #569cd6); }
1961
+ .hljs-meta { color: var(--muted); }
1962
+ .hljs-regexp { color: var(--vscode-symbolIcon-stringForeground, #d16969); }
1963
+
1964
+ /* Metric card */
1965
+ .metric-card {
1966
+ margin-top: 10px;
1967
+ border: 1px solid var(--success-border);
1968
+ background: var(--success-bg);
1969
+ border-radius: 10px;
1970
+ padding: 10px 14px;
1971
+ }
1972
+ .metric-card.partial {
1973
+ border-color: var(--warn-border, #d7a53c);
1974
+ background: var(--warn-bg, rgba(215,165,60,.1));
1975
+ }
1976
+ .metric-card.unavailable {
1977
+ border-color: var(--error-border, #e51400);
1978
+ background: var(--error-bg, rgba(229,20,0,.08));
1979
+ }
1980
+ .metric-header {
1981
+ color: var(--success); font-size: 11px; font-weight: 800;
1982
+ text-transform: uppercase; letter-spacing: .04em;
1983
+ margin-bottom: 4px;
1984
+ }
1985
+ .metric-card.partial .metric-header { color: var(--warn, #d7a53c); }
1986
+ .metric-card.unavailable .metric-header { color: var(--error, #e51400); }
1987
+ .metric-main { color: var(--success); font-weight: 700; font-size: 16px; }
1988
+ .metric-card.partial .metric-main { color: var(--warn, #d7a53c); }
1989
+ .metric-card.unavailable .metric-main { color: var(--error, #e51400); }
1990
+ .metric-detail { color: var(--muted); font-size: 11px; line-height: 1.5; margin-top: 2px; }
1991
+ .metric-status {
1992
+ font-size: 10px; font-weight: 600; margin-top: 4px;
1993
+ padding: 2px 8px; border-radius: 999px; display: inline-block;
1994
+ }
1995
+ .metric-status.complete { background: var(--success); color: #fff; }
1996
+ .metric-status.partial { background: var(--warn, #d7a53c); color: #000; }
1997
+ .metric-status.unavailable { background: var(--error, #e51400); color: #fff; }
1998
+ .metric-warnings { margin-top: 6px; font-size: 10px; color: var(--warn, #d7a53c); line-height: 1.4; }
1999
+ .metric-warning { display: flex; gap: 4px; align-items: flex-start; }
2000
+ .metric-warning::before { content: '!'; font-weight: 700; flex-shrink: 0; }
2001
+
2002
+ /* File list */
2003
+ .file-list { margin-top: 10px; display: flex; flex-direction: column; gap: 2px; }
2004
+ .file-row {
2005
+ display: flex; align-items: center; gap: 8px;
2006
+ padding: 5px 8px; border-radius: 6px;
2007
+ cursor: pointer; transition: background .12s; font-size: 12px;
2008
+ }
2009
+ .file-row:hover { background: var(--row-hover); }
2010
+ .file-row .path {
2011
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2012
+ font-family: var(--mono); font-size: 11px;
2013
+ }
2014
+ .file-row .tag {
2015
+ border-radius: 999px; padding: 1px 7px;
2016
+ font-size: 10px; font-weight: 600;
2017
+ border: 1px solid var(--border); white-space: nowrap;
2018
+ }
2019
+ .file-row .score { color: var(--muted); font-size: 10px; min-width: 32px; text-align: right; }
2020
+
2021
+ /* Feedback bar */
2022
+ .feedback-bar { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; }
2023
+ .feedback-label { width: 100%; color: var(--muted); margin-bottom: 2px; }
2024
+ .trace-list {
2025
+ margin-top: 10px;
2026
+ display: grid;
2027
+ gap: 4px;
2028
+ color: var(--muted);
2029
+ font-size: 11px;
2030
+ }
2031
+ .trace-row {
2032
+ display: grid;
2033
+ grid-template-columns: 14px minmax(0, 1fr);
2034
+ align-items: center;
2035
+ gap: 5px;
2036
+ }
2037
+ .trace-dot {
2038
+ width: 7px;
2039
+ height: 7px;
2040
+ border-radius: 999px;
2041
+ background: var(--success);
2042
+ justify-self: center;
2043
+ }
2044
+ .trace-text {
2045
+ overflow: hidden;
2046
+ text-overflow: ellipsis;
2047
+ white-space: nowrap;
2048
+ }
2049
+ .command-list {
2050
+ margin-top: 10px;
2051
+ display: grid;
2052
+ gap: 6px;
2053
+ }
2054
+ .command-row {
2055
+ display: grid;
2056
+ grid-template-columns: minmax(0, 1fr) auto;
2057
+ gap: 8px;
2058
+ align-items: center;
2059
+ border: 1px solid var(--hairline);
2060
+ border-radius: 7px;
2061
+ padding: 7px 8px;
2062
+ background: color-mix(in srgb, var(--panel) 42%, transparent);
2063
+ }
2064
+ .command-main {
2065
+ min-width: 0;
2066
+ }
2067
+ .command-text {
2068
+ display: block;
2069
+ overflow: hidden;
2070
+ text-overflow: ellipsis;
2071
+ white-space: nowrap;
2072
+ font-family: var(--mono);
2073
+ font-size: 11px;
2074
+ }
2075
+ .command-reason {
2076
+ display: block;
2077
+ margin-top: 2px;
2078
+ overflow: hidden;
2079
+ text-overflow: ellipsis;
2080
+ white-space: nowrap;
2081
+ color: var(--muted);
2082
+ font-size: 10px;
2083
+ }
2084
+
2085
+ /* Composer */
2086
+ .composer {
2087
+ flex-shrink: 0;
2088
+ border-top: 1px solid var(--hairline);
2089
+ padding: 9px 10px 10px;
2090
+ background: color-mix(in srgb, var(--panel) 36%, var(--bg));
2091
+ }
2092
+ .composer-box {
2093
+ border: 1px solid var(--hairline);
2094
+ border-radius: 10px;
2095
+ background: color-mix(in srgb, var(--input) 86%, var(--panel));
2096
+ overflow: hidden;
2097
+ transition: border-color .2s, box-shadow .2s;
2098
+ }
2099
+ .composer-box:focus-within {
2100
+ border-color: color-mix(in srgb, var(--vscode-foreground) 28%, var(--hairline));
2101
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--vscode-foreground) 8%, transparent);
2102
+ }
2103
+ .composer-box textarea {
2104
+ width: 100%; border: 0; border-radius: 0;
2105
+ background: transparent;
2106
+ color: var(--vscode-input-foreground);
2107
+ font-family: var(--font); font-size: 13px; line-height: 1.5;
2108
+ padding: 10px 11px 4px;
2109
+ min-height: 56px; max-height: 190px;
2110
+ resize: none; outline: none; overflow-y: auto;
2111
+ }
2112
+ .composer-box textarea::placeholder { color: var(--muted); opacity: .65; }
2113
+ .composer-toolbar {
2114
+ display: grid;
2115
+ grid-template-columns: minmax(0, 1fr) auto 28px;
2116
+ grid-template-areas: "model mode action";
2117
+ align-items: center;
2118
+ gap: 6px;
2119
+ padding: 3px 8px 8px;
2120
+ }
2121
+ .composer-toolbar .spacer { display: none; }
2122
+ .model-select {
2123
+ grid-area: model;
2124
+ position: relative;
2125
+ width: 100%;
2126
+ min-width: 0;
2127
+ max-width: none;
2128
+ }
2129
+ .model-picker {
2130
+ width: 100%;
2131
+ min-width: 0;
2132
+ display: grid;
2133
+ grid-template-columns: minmax(0, 1fr) auto;
2134
+ align-items: center;
2135
+ gap: 6px;
2136
+ border: 1px solid transparent;
2137
+ background: color-mix(in srgb, var(--vscode-foreground) 5%, transparent);
2138
+ color: var(--muted);
2139
+ font-family: var(--font);
2140
+ font-size: 11px;
2141
+ font-weight: 600;
2142
+ padding: 4px 7px;
2143
+ border-radius: 7px;
2144
+ outline: none;
2145
+ cursor: pointer;
2146
+ text-align: left;
2147
+ }
2148
+ .model-picker:hover:not(:disabled),
2149
+ .model-picker[aria-expanded="true"] {
2150
+ color: var(--vscode-foreground);
2151
+ background: var(--surface-hover);
2152
+ border-color: var(--hairline);
2153
+ }
2154
+ .model-picker:disabled { opacity: .55; cursor: default; }
2155
+ .model-picker-label {
2156
+ overflow: hidden;
2157
+ text-overflow: ellipsis;
2158
+ white-space: nowrap;
2159
+ }
2160
+ .model-chevron {
2161
+ color: var(--muted);
2162
+ font-size: 10px;
2163
+ line-height: 1;
2164
+ }
2165
+ .model-menu {
2166
+ position: fixed;
2167
+ width: min(290px, calc(100vw - 24px));
2168
+ max-height: 300px;
2169
+ display: none;
2170
+ flex-direction: column;
2171
+ gap: 2px;
2172
+ padding: 6px;
2173
+ overflow-y: auto;
2174
+ z-index: 100;
2175
+ border: 1px solid var(--hairline);
2176
+ border-radius: 9px;
2177
+ background: var(--panel);
2178
+ box-shadow: 0 10px 30px color-mix(in srgb, #000 35%, transparent);
2179
+ }
2180
+ .model-menu.open { display: flex; }
2181
+ .model-option {
2182
+ width: 100%;
2183
+ display: grid;
2184
+ grid-template-columns: auto minmax(0, 1fr) auto;
2185
+ gap: 8px;
2186
+ align-items: center;
2187
+ border: 0;
2188
+ border-radius: 7px;
2189
+ background: transparent;
2190
+ color: var(--vscode-foreground);
2191
+ padding: 7px 8px;
2192
+ text-align: left;
2193
+ cursor: pointer;
2194
+ }
2195
+ .model-option:hover { background: var(--surface-hover); }
2196
+ .model-option.active { background: var(--row-active); }
2197
+ .model-option-dot {
2198
+ width: 7px;
2199
+ height: 7px;
2200
+ border-radius: 999px;
2201
+ background: color-mix(in srgb, var(--muted) 55%, transparent);
2202
+ }
2203
+ .model-option.connected .model-option-dot { background: var(--vscode-foreground); }
2204
+ .model-option-main {
2205
+ display: block;
2206
+ min-width: 0;
2207
+ }
2208
+ .model-option-name {
2209
+ display: block;
2210
+ overflow: hidden;
2211
+ text-overflow: ellipsis;
2212
+ white-space: nowrap;
2213
+ font-size: 12px;
2214
+ font-weight: 650;
2215
+ }
2216
+ .model-option-meta {
2217
+ display: block;
2218
+ margin-top: 1px;
2219
+ overflow: hidden;
2220
+ text-overflow: ellipsis;
2221
+ white-space: nowrap;
2222
+ color: var(--muted);
2223
+ font-family: var(--mono);
2224
+ font-size: 10px;
2225
+ font-weight: 400;
2226
+ }
2227
+ .model-option-check {
2228
+ width: 12px;
2229
+ height: 12px;
2230
+ color: var(--muted);
2231
+ opacity: 0;
2232
+ }
2233
+ .model-option-check::before {
2234
+ content: "";
2235
+ display: block;
2236
+ width: 5px;
2237
+ height: 9px;
2238
+ margin: 0 auto;
2239
+ border: solid currentColor;
2240
+ border-width: 0 1.5px 1.5px 0;
2241
+ transform: rotate(45deg);
2242
+ }
2243
+ .model-option.active .model-option-check { opacity: 1; }
2244
+
2245
+ /* Buttons */
2246
+ button {
2247
+ border: 0; border-radius: 7px;
2248
+ cursor: pointer;
2249
+ font-family: var(--font); font-size: 12px; font-weight: 600;
2250
+ padding: 5px 12px;
2251
+ transition: opacity .15s, background .15s, color .15s;
2252
+ white-space: nowrap;
2253
+ }
2254
+ button:disabled { cursor: default; opacity: .5; }
2255
+ button.primary {
2256
+ background: var(--button);
2257
+ color: var(--button-fg);
2258
+ border: 1px solid color-mix(in srgb, var(--vscode-foreground) 18%, transparent);
2259
+ }
2260
+ button.primary:hover:not(:disabled) { background: var(--button-hover); opacity: 1; }
2261
+ button.secondary { background: var(--secondary-button); color: var(--secondary-button-fg); }
2262
+ button.secondary:hover:not(:disabled) { opacity: .85; }
2263
+ button.ghost { background: transparent; color: var(--muted); }
2264
+ button.ghost:hover:not(:disabled) { background: var(--row-hover); color: var(--vscode-foreground); }
2265
+ button.positive { background: var(--success); color: #fff; }
2266
+ button.tiny { padding: 3px 8px; font-size: 11px; border-radius: 5px; }
2267
+
2268
+ /* Mode dropdown */
2269
+ .mode-select {
2270
+ grid-area: mode;
2271
+ position: relative;
2272
+ min-width: 118px;
2273
+ }
2274
+ .mode-picker {
2275
+ width: 100%;
2276
+ min-width: 0;
2277
+ display: grid;
2278
+ grid-template-columns: minmax(0, 1fr) auto;
2279
+ align-items: center;
2280
+ gap: 6px;
2281
+ border: 1px solid var(--hairline);
2282
+ background: color-mix(in srgb, var(--vscode-foreground) 5%, transparent);
2283
+ color: var(--muted);
2284
+ font-family: var(--font);
2285
+ font-size: 11px;
2286
+ font-weight: 600;
2287
+ padding: 4px 7px;
2288
+ border-radius: 7px;
2289
+ outline: none;
2290
+ cursor: pointer;
2291
+ text-align: left;
2292
+ }
2293
+ .mode-picker:hover,
2294
+ .mode-picker[aria-expanded="true"] {
2295
+ color: var(--vscode-foreground);
2296
+ background: var(--surface-hover);
2297
+ border-color: var(--hairline);
2298
+ }
2299
+ .mode-picker-label {
2300
+ overflow: hidden;
2301
+ text-overflow: ellipsis;
2302
+ white-space: nowrap;
2303
+ }
2304
+ .mode-menu {
2305
+ position: fixed;
2306
+ width: min(220px, calc(100vw - 24px));
2307
+ display: none;
2308
+ flex-direction: column;
2309
+ gap: 2px;
2310
+ padding: 6px;
2311
+ z-index: 100;
2312
+ border: 1px solid var(--hairline);
2313
+ border-radius: 9px;
2314
+ background: var(--panel);
2315
+ box-shadow: 0 10px 30px color-mix(in srgb, #000 35%, transparent);
2316
+ }
2317
+ .mode-menu.open { display: flex; }
2318
+ .mode-option {
2319
+ width: 100%;
2320
+ display: grid;
2321
+ grid-template-columns: minmax(0, 1fr) auto;
2322
+ gap: 8px;
2323
+ align-items: center;
2324
+ border: 0;
2325
+ border-radius: 7px;
2326
+ background: transparent;
2327
+ color: var(--vscode-foreground);
2328
+ padding: 7px 8px;
2329
+ text-align: left;
2330
+ cursor: pointer;
2331
+ }
2332
+ .mode-option:hover { background: var(--surface-hover); }
2333
+ .mode-option.active { background: var(--row-active); }
2334
+ .mode-option-main { min-width: 0; }
2335
+ .mode-option-name { display: block; font-size: 12px; font-weight: 650; }
2336
+ .mode-option-meta {
2337
+ display: block;
2338
+ margin-top: 1px;
2339
+ overflow: hidden;
2340
+ text-overflow: ellipsis;
2341
+ white-space: nowrap;
2342
+ color: var(--muted);
2343
+ font-size: 10px;
2344
+ font-weight: 400;
2345
+ }
2346
+ .mode-option-check { width: 12px; height: 12px; color: var(--muted); opacity: 0; }
2347
+ .mode-option-check::before {
2348
+ content: "";
2349
+ display: block;
2350
+ width: 5px;
2351
+ height: 9px;
2352
+ margin: 0 auto;
2353
+ border: solid currentColor;
2354
+ border-width: 0 1.5px 1.5px 0;
2355
+ transform: rotate(45deg);
2356
+ }
2357
+ .mode-option.active .mode-option-check { opacity: 1; }
2358
+
2359
+ .stop-btn, .send-btn {
2360
+ grid-area: action;
2361
+ justify-self: end;
2362
+ width: 28px; height: 28px;
2363
+ min-width: 28px;
2364
+ border-radius: 7px;
2365
+ display: flex; align-items: center; justify-content: center;
2366
+ flex-shrink: 0; font-size: 14px; padding: 0;
2367
+ cursor: pointer;
2368
+ transition: background .15s, color .15s, border-color .15s;
2369
+ }
2370
+ .stop-btn {
2371
+ background: transparent;
2372
+ border: 1px solid var(--hairline);
2373
+ color: var(--muted);
2374
+ font-size: 10px;
2375
+ }
2376
+ .stop-btn:hover { background: var(--surface-hover); color: var(--vscode-foreground); }
2377
+ .send-btn {
2378
+ background: var(--button);
2379
+ color: var(--button-fg);
2380
+ border: 1px solid color-mix(in srgb, var(--vscode-foreground) 18%, transparent);
2381
+ }
2382
+ .send-btn:hover:not(:disabled) { background: var(--button-hover); }
2383
+ .send-btn:disabled { opacity: .4; }
2384
+
2385
+ @media (max-width: 340px) {
2386
+ .composer { padding: 8px; }
2387
+ .composer-box textarea {
2388
+ min-height: 50px;
2389
+ padding: 9px 10px 3px;
2390
+ }
2391
+ .composer-toolbar {
2392
+ grid-template-columns: minmax(0, 1fr) 28px;
2393
+ grid-template-areas:
2394
+ "model action"
2395
+ "mode mode";
2396
+ gap: 6px;
2397
+ padding: 3px 8px 8px;
2398
+ }
2399
+ .mode-select {
2400
+ width: 100%;
2401
+ min-width: 0;
2402
+ }
2403
+ .mode-menu {
2404
+ width: calc(100vw - 20px);
2405
+ }
2406
+ .model-menu {
2407
+ width: calc(100vw - 20px);
2408
+ max-height: 220px;
2409
+ }
2410
+ }
2411
+
2412
+ /* Hover actions on messages */
2413
+ .msg-actions {
2414
+ display: flex; gap: 4px; margin-top: 6px;
2415
+ opacity: 0; transition: opacity .2s;
2416
+ }
2417
+ .msg-body:hover .msg-actions { opacity: 1; }
2418
+ .msg-row.user .msg-actions { flex-direction: row-reverse; }
2419
+ </style>
2420
+ </head>
2421
+ <body>
2422
+ <div class="header">
2423
+ <div class="brand"><img src="${logoUri}" alt=""></div>
2424
+ <div class="title wordmark" data-text="FlowSeeker">FlowSeeker</div>
2425
+ <div class="spacer"></div>
2426
+ <button id="newThread" title="New chat">+</button>
2427
+ <button id="toggleThreads" title="Chat history">&#9776;</button>
2428
+ <button id="toggleLogin" title="AI Provider">&#9881;</button>
2429
+ </div>
2430
+ <div class="status-line" id="connection">
2431
+ <span class="status-badge" id="statusBadge">Disconnected</span>
2432
+ <span class="status-text" id="statusText">Checking AI provider...</span>
2433
+ </div>
2434
+ <div class="thread-panel" id="threadPanel">
2435
+ <div class="thread-panel-head">
2436
+ <span>Chat history</span>
2437
+ <div class="spacer"></div>
2438
+ <button class="ghost tiny" id="newThreadPanel">New</button>
2439
+ </div>
2440
+ <div class="thread-list" id="threadList"></div>
2441
+ </div>
2442
+ <div class="login-panel" id="login">
2443
+ <div class="panel-head">
2444
+ <div class="panel-head-main">
2445
+ <div class="panel-head-title">AI Provider</div>
2446
+ <div class="panel-head-subtitle">Connect model profiles without writing status messages into chat.</div>
2447
+ </div>
2448
+ <button class="panel-close" id="closeLogin" title="Close AI Provider">x</button>
2449
+ </div>
2450
+ <div class="gateway-status" id="gatewayStatus">
2451
+ <span class="gateway-dot" aria-hidden="true"></span>
2452
+ <span class="gateway-status-text" id="gatewayStatusText">Disconnected · local retrieval only</span>
2453
+ </div>
2454
+ <div class="gateway-feedback" id="gatewayFeedback" role="status"></div>
2455
+ <div class="profile-list" id="profileList"></div>
2456
+ <label>Provider
2457
+ <select id="provider">${providerOptions}</select>
2458
+ </label>
2459
+ <label><span id="modelLabel">Model</span>
2460
+ <input id="model" placeholder="Model">
2461
+ </label>
2462
+ <label><span id="baseUrlLabel">Base URL</span>
2463
+ <input id="baseUrl" placeholder="Provider base URL">
2464
+ </label>
2465
+ <label id="apiKeyLabel">API Key / Token
2466
+ <input id="token" placeholder="API key" type="password">
2467
+ </label>
2468
+ <label id="modelSelectLabel" style="display:none">Model
2469
+ <select id="oauthModelSelect"></select>
2470
+ </label>
2471
+ <div class="login-row">
2472
+ <button class="primary" id="connect">Save profile</button>
2473
+ <button class="secondary" id="fetchModels">Load models</button>
2474
+ <button class="secondary" id="testProvider">Test</button>
2475
+ <button class="secondary" id="disconnect">Disconnect</button>
2476
+ <button class="primary" id="oauthAuthorize" style="display:none">Sign in with browser</button>
2477
+ </div>
2478
+ </div>
2479
+ <div class="agent-control-panel" id="agentControls">
2480
+ <div class="panel-head">
2481
+ <div class="panel-head-main">
2482
+ <div class="panel-head-title">Agent controls</div>
2483
+ <div class="panel-head-subtitle">Separate workspace, command, and MCP permissions.</div>
2484
+ </div>
2485
+ <button class="panel-close" id="closeAgentControls" title="Close agent controls">x</button>
2486
+ </div>
2487
+ <label class="toggle-row">
2488
+ <input id="policyReadWorkspace" type="checkbox">
2489
+ <span><strong>Read workspace</strong><span>Allow Solve Packet retrieval and workspace reads without asking.</span></span>
2490
+ </label>
2491
+ <label class="toggle-row">
2492
+ <input id="policyEditWorkspace" type="checkbox">
2493
+ <span><strong>Auto-apply edits</strong><span>Skip the final Apply button after an approved plan.</span></span>
2494
+ </label>
2495
+ <label class="toggle-row">
2496
+ <input id="policySafeCommands" type="checkbox">
2497
+ <span><strong>Auto-run safe checks</strong><span>Allow build, lint, test, status, and typecheck commands.</span></span>
2498
+ </label>
2499
+ <label class="toggle-row">
2500
+ <input id="policyAllCommands" type="checkbox">
2501
+ <span><strong>Auto-run other commands</strong><span>Higher risk. Destructive commands still require explicit approval.</span></span>
2502
+ </label>
2503
+ <label class="toggle-row">
2504
+ <input id="policyMcpTools" type="checkbox">
2505
+ <span><strong>Auto-use MCP tools</strong><span>Allow configured MCP tools without a separate approval prompt.</span></span>
2506
+ </label>
2507
+ <label class="agent-rules">Rules
2508
+ <textarea id="agentRules" placeholder="One rule per line. Example: Always explain missing context before asking for more files."></textarea>
2509
+ </label>
2510
+ <div class="login-row">
2511
+ <button class="primary" id="saveAgentPolicy">Save agent controls</button>
2512
+ <button class="secondary" id="openMcpConfig">MCP config</button>
2513
+ </div>
2514
+ </div>
2515
+ <div class="messages" id="messages">
2516
+ <div class="empty-state" id="empty">
2517
+ <div class="brand-lg"><img src="${logoUri}" alt=""></div>
2518
+ <div class="empty-title"><span class="wordmark" data-text="FlowSeeker">FlowSeeker</span></div>
2519
+ <div class="empty-hint">New session</div>
2520
+ </div>
2521
+ </div>
2522
+ <div class="agent-policy" id="agentPolicy">
2523
+ <div class="agent-policy-main">
2524
+ <span class="agent-policy-label">Agent</span>
2525
+ <span class="agent-policy-text" id="agentPolicyText">Review edits before apply</span>
2526
+ </div>
2527
+ <div class="agent-policy-actions">
2528
+ <button id="openAgentSettings" title="Open FlowSeeker agent settings">Settings</button>
2529
+ <button id="toggleAgentControls" class="ghost" title="Agent controls">Controls</button>
2530
+ </div>
2531
+ <div class="quota-panel" id="quotaPanel">
2532
+ <div class="quota-header" id="quotaHeader">
2533
+ <span class="chevron" id="quotaChevron">&#9654;</span>
2534
+ <span class="quota-summary-text" id="quotaSummaryText">No usage yet</span>
2535
+ <span class="quota-badge ok" id="quotaBadge">0%</span>
2536
+ </div>
2537
+ <div class="quota-detail" id="quotaDetail"></div>
2538
+ </div>
2539
+ </div>
2540
+ <div class="composer">
2541
+ <div class="composer-box">
2542
+ <textarea id="task" placeholder="Ask FlowSeeker to find the relevant code..."></textarea>
2543
+ <div class="composer-toolbar">
2544
+ <div class="model-select" id="modelSelect">
2545
+ <button class="model-picker" id="modelPicker" type="button" title="Active AI model" aria-haspopup="listbox" aria-expanded="false">
2546
+ <span class="model-picker-label" id="modelPickerLabel">No AI provider</span>
2547
+ <span class="model-chevron">v</span>
2548
+ </button>
2549
+ </div>
2550
+ <div class="spacer"></div>
2551
+ <div class="mode-select" id="modeSelect">
2552
+ <button class="mode-picker" id="modePicker" type="button" title="FlowSeeker mode" aria-haspopup="listbox" aria-expanded="false">
2553
+ <span class="mode-picker-label" id="modePickerLabel">Guide</span>
2554
+ <span class="model-chevron">v</span>
2555
+ </button>
2556
+ </div>
2557
+ <button class="stop-btn" id="stopBtn" title="Stop generating" style="display:none">&#9632;</button>
2558
+ <button class="send-btn" id="send" title="Send (Enter)">&#8593;</button>
2559
+ </div>
2560
+ </div>
2561
+ </div>
2562
+ <div class="model-menu" id="modelMenu" role="listbox"></div>
2563
+ <div class="mode-menu" id="modeMenu" role="listbox"></div>
2564
+ <script nonce="${nonce}">
2565
+ const vscode = acquireVsCodeApi();
2566
+ const providerMeta = ${providerMeta};
2567
+ const messages = document.getElementById('messages');
2568
+ const connection = document.getElementById('connection');
2569
+ const statusBadge = document.getElementById('statusBadge');
2570
+ const connectionStatusText = document.getElementById('statusText');
2571
+ const agentPolicy = document.getElementById('agentPolicy');
2572
+ const agentPolicyText = document.getElementById('agentPolicyText');
2573
+ const gatewayStatus = document.getElementById('gatewayStatus');
2574
+ const gatewayStatusText = document.getElementById('gatewayStatusText');
2575
+ const gatewayFeedback = document.getElementById('gatewayFeedback');
2576
+ const login = document.getElementById('login');
2577
+ const threadPanel = document.getElementById('threadPanel');
2578
+ const threadList = document.getElementById('threadList');
2579
+ const modelPicker = document.getElementById('modelPicker');
2580
+ const modelPickerLabel = document.getElementById('modelPickerLabel');
2581
+ const modelMenu = document.getElementById('modelMenu');
2582
+ const profileList = document.getElementById('profileList');
2583
+ const provider = document.getElementById('provider');
2584
+ const modelLabel = document.getElementById('modelLabel');
2585
+ const model = document.getElementById('model');
2586
+ const baseUrlLabel = document.getElementById('baseUrlLabel');
2587
+ const baseUrl = document.getElementById('baseUrl');
2588
+ const token = document.getElementById('token');
2589
+ const task = document.getElementById('task');
2590
+ const modePicker = document.getElementById('modePicker');
2591
+ const modePickerLabel = document.getElementById('modePickerLabel');
2592
+ const modeMenu = document.getElementById('modeMenu');
2593
+ const sendButton = document.getElementById('send');
2594
+ const stopBtn = document.getElementById('stopBtn');
2595
+ const policyReadWorkspace = document.getElementById('policyReadWorkspace');
2596
+ const policyEditWorkspace = document.getElementById('policyEditWorkspace');
2597
+ const policySafeCommands = document.getElementById('policySafeCommands');
2598
+ const policyAllCommands = document.getElementById('policyAllCommands');
2599
+ const policyMcpTools = document.getElementById('policyMcpTools');
2600
+ const agentRules = document.getElementById('agentRules');
2601
+ let agentState = 'idle';
2602
+ let busy = false;
2603
+ const ACTION_STATES = new Set(['approving_plan', 'applying_edits', 'verifying', 'running_command', 'running_mcp_tool']);
2604
+ let currentMode = 'guide';
2605
+ const modeOptions = [
2606
+ { id: 'guide', label: 'Guide', hint: 'Retrieve context, then explain likely changes.' },
2607
+ { id: 'auto', label: 'Plan', hint: 'Create a reviewable implementation plan.' },
2608
+ { id: 'chat', label: 'Chat', hint: 'Talk directly to connected AI provider.' }
2609
+ ];
2610
+ let activeThreadId = '';
2611
+ let activeProfileId = '';
2612
+ let providerProfiles = [];
2613
+ let providerModelOptions = [];
2614
+ let providerModelOptionsProvider = '';
2615
+ let modelFetchTimer = 0;
2616
+ let activeStreamingRow = null;
2617
+ let activeStreamingContent = null;
2618
+ let activeStreamingText = '';
2619
+
2620
+ document.getElementById('newThread').addEventListener('click', function() { if (!busy) vscode.postMessage({ type: 'newThread' }); });
2621
+ document.getElementById('newThreadPanel').addEventListener('click', function() { if (!busy) vscode.postMessage({ type: 'newThread' }); });
2622
+ document.getElementById('toggleThreads').addEventListener('click', function() { threadPanel.classList.toggle('open'); });
2623
+ document.getElementById('quotaHeader').addEventListener('click', function() {
2624
+ var detail = document.getElementById('quotaDetail');
2625
+ var chevron = document.getElementById('quotaChevron');
2626
+ var open = detail.classList.toggle('open');
2627
+ chevron.classList.toggle('open', open);
2628
+ });
2629
+ document.getElementById('toggleLogin').addEventListener('click', function() {
2630
+ login.classList.toggle('open');
2631
+ if (login.classList.contains('open')) document.getElementById('agentControls').classList.remove('open');
2632
+ });
2633
+ document.getElementById('closeLogin').addEventListener('click', function() { login.classList.remove('open'); });
2634
+ document.getElementById('openAgentSettings').addEventListener('click', function() { vscode.postMessage({ type: 'openAgentSettings' }); });
2635
+ document.getElementById('toggleAgentControls').addEventListener('click', function() {
2636
+ var ctrl = document.getElementById('agentControls');
2637
+ ctrl.classList.toggle('open');
2638
+ if (ctrl.classList.contains('open')) login.classList.remove('open');
2639
+ });
2640
+ document.getElementById('closeAgentControls').addEventListener('click', function() { document.getElementById('agentControls').classList.remove('open'); });
2641
+ modelPicker.addEventListener('click', function(event) {
2642
+ event.stopPropagation();
2643
+ if (!providerProfiles.length) return;
2644
+ setModeMenuOpen(false);
2645
+ setModelMenuOpen(!modelMenu.classList.contains('open'));
2646
+ });
2647
+ modelMenu.addEventListener('click', function(event) {
2648
+ event.stopPropagation();
2649
+ });
2650
+ modePicker.addEventListener('click', function(event) {
2651
+ event.stopPropagation();
2652
+ setModelMenuOpen(false);
2653
+ setModeMenuOpen(!modeMenu.classList.contains('open'));
2654
+ });
2655
+ modeMenu.addEventListener('click', function(event) {
2656
+ event.stopPropagation();
2657
+ });
2658
+ document.addEventListener('click', function() {
2659
+ setModelMenuOpen(false);
2660
+ setModeMenuOpen(false);
2661
+ });
2662
+ document.addEventListener('keydown', function(event) {
2663
+ if (event.key === 'Escape') {
2664
+ setModelMenuOpen(false);
2665
+ setModeMenuOpen(false);
2666
+ }
2667
+ });
2668
+ document.getElementById('connect').addEventListener('click', function() {
2669
+ setGatewayTesting('Saving provider profile...');
2670
+ vscode.postMessage(providerPayload('connect'));
2671
+ });
2672
+ document.getElementById('fetchModels').addEventListener('click', function() {
2673
+ fetchProviderModels();
2674
+ });
2675
+ document.getElementById('testProvider').addEventListener('click', function() {
2676
+ setGatewayTesting('Testing AI provider...');
2677
+ vscode.postMessage(providerPayload('testProvider'));
2678
+ });
2679
+ document.getElementById('oauthAuthorize').addEventListener('click', function() {
2680
+ vscode.postMessage({ type: 'startOAuth', provider: provider.value });
2681
+ });
2682
+ document.getElementById('disconnect').addEventListener('click', function() {
2683
+ vscode.postMessage({ type: 'disconnect' });
2684
+ });
2685
+ document.getElementById('saveAgentPolicy').addEventListener('click', function() {
2686
+ vscode.postMessage(agentPolicyPayload());
2687
+ });
2688
+ document.getElementById('openMcpConfig').addEventListener('click', function() {
2689
+ vscode.postMessage({ type: 'openMcpConfig' });
2690
+ });
2691
+ document.getElementById('send').addEventListener('click', send);
2692
+ document.getElementById('stopBtn').addEventListener('click', function() { vscode.postMessage({ type: 'cancel' }); setAgentState('cancelled'); });
2693
+ renderModeMenu();
2694
+ provider.addEventListener('change', function() {
2695
+ providerModelOptions = [];
2696
+ providerModelOptionsProvider = provider.value;
2697
+ applyProviderDefaults(true);
2698
+ renderModelDropdown([], model.value, provider.value);
2699
+ scheduleModelFetch();
2700
+ });
2701
+ baseUrl.addEventListener('input', scheduleModelFetch);
2702
+ token.addEventListener('input', function() { token.classList.remove('key-saved'); token.placeholder = 'API key'; scheduleModelFetch(); });
2703
+ var modelSelectEl = document.getElementById('oauthModelSelect');
2704
+ if (modelSelectEl) {
2705
+ modelSelectEl.addEventListener('change', function() {
2706
+ model.value = modelSelectEl.value;
2707
+ });
2708
+ }
2709
+ task.addEventListener('keydown', function(event) {
2710
+ if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); send(); }
2711
+ });
2712
+
2713
+ function getModelValue() {
2714
+ var sel = document.getElementById('oauthModelSelect');
2715
+ var selectLabel = document.getElementById('modelSelectLabel');
2716
+ return sel && selectLabel && selectLabel.style.display !== 'none' && sel.value ? sel.value : model.value;
2717
+ }
2718
+
2719
+ function providerPayload(type) {
2720
+ return {
2721
+ type: type,
2722
+ provider: provider.value,
2723
+ model: getModelValue(),
2724
+ baseUrl: baseUrl.value,
2725
+ endpoint: baseUrl.value,
2726
+ token: token.value
2727
+ };
2728
+ }
2729
+
2730
+ function agentPolicyPayload() {
2731
+ return {
2732
+ type: 'updateAgentPolicy',
2733
+ readWorkspace: Boolean(policyReadWorkspace && policyReadWorkspace.checked),
2734
+ editWorkspace: Boolean(policyEditWorkspace && policyEditWorkspace.checked),
2735
+ safeCommands: Boolean(policySafeCommands && policySafeCommands.checked),
2736
+ allCommands: Boolean(policyAllCommands && policyAllCommands.checked),
2737
+ mcpTools: Boolean(policyMcpTools && policyMcpTools.checked),
2738
+ rulesText: agentRules ? agentRules.value : ''
2739
+ };
2740
+ }
2741
+
2742
+ function applyProviderDefaults(force) {
2743
+ var meta = providerMeta[provider.value] || providerMeta.openai;
2744
+ if (force || !model.value) model.value = meta.defaultModel || '';
2745
+ if (force || !baseUrl.value) baseUrl.value = meta.defaultBaseUrl || '';
2746
+ modelLabel.textContent = meta.modelLabel || 'Model';
2747
+ baseUrlLabel.textContent = meta.baseUrlLabel || 'Base URL';
2748
+ model.placeholder = meta.modelPlaceholder || 'Model';
2749
+ baseUrl.placeholder = meta.baseUrlPlaceholder || 'Provider base URL';
2750
+ token.placeholder = meta.apiKeyLabel || 'API key';
2751
+ if (meta.apiKeyHint) token.placeholder = (meta.apiKeyLabel || 'API key') + ' (' + meta.apiKeyHint + ')';
2752
+ token.disabled = !meta.requiresApiKey;
2753
+ if (!meta.requiresApiKey) token.value = '';
2754
+
2755
+ var isOAuth = meta.authMethod === 'oauth' && meta.oauthConfig && meta.oauthConfig.clientId;
2756
+ var hasProviderModelOptions = meta.supportsModelList;
2757
+ var apiKeyLabel = document.getElementById('apiKeyLabel');
2758
+ var modelSelectLabel = document.getElementById('modelSelectLabel');
2759
+ var oauthBtn = document.getElementById('oauthAuthorize');
2760
+ var connectBtn = document.getElementById('connect');
2761
+ var testBtn = document.getElementById('testProvider');
2762
+ var fetchModelsBtn = document.getElementById('fetchModels');
2763
+ var modelInput = document.getElementById('model');
2764
+
2765
+ if (apiKeyLabel) apiKeyLabel.style.display = meta.requiresApiKey ? '' : 'none';
2766
+ if (oauthBtn) {
2767
+ oauthBtn.style.display = isOAuth ? '' : 'none';
2768
+ oauthBtn.textContent = 'Sign in with browser';
2769
+ oauthBtn.className = 'primary';
2770
+ oauthBtn.title = 'Authorize this provider in your browser.';
2771
+ }
2772
+ if (connectBtn) connectBtn.style.display = isOAuth ? 'none' : '';
2773
+ if (testBtn) testBtn.style.display = isOAuth ? 'none' : '';
2774
+ if (fetchModelsBtn) fetchModelsBtn.style.display = meta.supportsModelList && !isOAuth ? '' : 'none';
2775
+
2776
+ if (hasProviderModelOptions && modelSelectLabel) {
2777
+ modelSelectLabel.style.display = '';
2778
+ if (modelInput) modelInput.style.display = 'none';
2779
+ } else {
2780
+ if (modelSelectLabel) modelSelectLabel.style.display = 'none';
2781
+ if (modelInput) modelInput.style.display = '';
2782
+ }
2783
+ }
2784
+
2785
+ function scheduleModelFetch() {
2786
+ window.clearTimeout(modelFetchTimer);
2787
+ modelFetchTimer = window.setTimeout(fetchProviderModels, 650);
2788
+ }
2789
+
2790
+ function fetchProviderModels() {
2791
+ var meta = providerMeta[provider.value] || providerMeta.openai;
2792
+ if (!meta.supportsModelList) return;
2793
+ if (meta.requiresApiKey && token.value.trim().length < 6) {
2794
+ renderModelDropdown([], model.value || meta.defaultModel || '', provider.value);
2795
+ return;
2796
+ }
2797
+ vscode.postMessage(providerPayload('fetchModels'));
2798
+ }
2799
+
2800
+ function setGatewayTesting(text) {
2801
+ if (!gatewayStatus.classList.contains('testing')) {
2802
+ gatewayStatus.dataset.previousText = gatewayStatusText.textContent || '';
2803
+ }
2804
+ gatewayStatus.classList.add('testing');
2805
+ gatewayStatusText.textContent = text || 'Testing AI provider...';
2806
+ showGatewayFeedback('info', text || 'Testing AI provider...', false);
2807
+ }
2808
+
2809
+ function showGatewayFeedback(status, text, autoHide) {
2810
+ if (status !== 'info' || (text || '').toLowerCase().indexOf('testing') < 0) {
2811
+ gatewayStatus.classList.remove('testing');
2812
+ if (gatewayStatus.dataset.previousText) {
2813
+ gatewayStatusText.textContent = gatewayStatus.dataset.previousText;
2814
+ gatewayStatus.dataset.previousText = '';
2815
+ }
2816
+ }
2817
+ gatewayFeedback.className = 'gateway-feedback show ' + (status || 'info');
2818
+ gatewayFeedback.textContent = text || '';
2819
+ if (autoHide !== false && status !== 'error') {
2820
+ window.clearTimeout(showGatewayFeedback.timer || 0);
2821
+ showGatewayFeedback.timer = window.setTimeout(function() {
2822
+ gatewayFeedback.className = 'gateway-feedback';
2823
+ gatewayFeedback.textContent = '';
2824
+ }, 5000);
2825
+ }
2826
+ }
2827
+
2828
+ function renderGatewayConnectionStatus(connected, label, modelName) {
2829
+ gatewayStatus.classList.remove('testing');
2830
+ gatewayStatus.dataset.previousText = '';
2831
+ gatewayStatus.classList.toggle('connected', Boolean(connected));
2832
+ gatewayStatusText.textContent = connected
2833
+ ? 'Connected · ' + (label || 'AI provider') + (modelName ? ' / ' + modelName : '')
2834
+ : 'Disconnected · local retrieval only';
2835
+ }
2836
+
2837
+ function renderProviderProfiles(profiles, activeId) {
2838
+ providerProfiles = Array.isArray(profiles) ? profiles : [];
2839
+ activeProfileId = activeId || '';
2840
+ modelMenu.innerHTML = '';
2841
+ profileList.innerHTML = '';
2842
+
2843
+ if (!providerProfiles.length) {
2844
+ modelPickerLabel.textContent = 'No AI provider';
2845
+ modelPicker.disabled = true;
2846
+ setModelMenuOpen(false);
2847
+ var empty = document.createElement('div');
2848
+ empty.className = 'profile-empty';
2849
+ empty.textContent = 'No saved provider profiles.';
2850
+ profileList.appendChild(empty);
2851
+ return;
2852
+ }
2853
+
2854
+ modelPicker.disabled = false;
2855
+ if (!providerProfiles.some(function(profile) { return profile.id === activeProfileId; })) {
2856
+ activeProfileId = providerProfiles[0].id;
2857
+ }
2858
+ providerProfiles.forEach(function(profile) {
2859
+ var option = document.createElement('button');
2860
+ option.type = 'button';
2861
+ option.className = 'model-option' + (profile.id === activeProfileId ? ' active' : '') + (profile.connected ? ' connected' : '');
2862
+ option.setAttribute('role', 'option');
2863
+ option.setAttribute('aria-selected', profile.id === activeProfileId ? 'true' : 'false');
2864
+ option.title = fullProfileLabel(profile);
2865
+ option.addEventListener('click', function() {
2866
+ setModelMenuOpen(false);
2867
+ if (profile.id !== activeProfileId) vscode.postMessage({ type: 'selectProviderProfile', profileId: profile.id });
2868
+ });
2869
+
2870
+ var optionDot = document.createElement('span');
2871
+ optionDot.className = 'model-option-dot';
2872
+ var optionMain = document.createElement('span');
2873
+ optionMain.className = 'model-option-main';
2874
+ var optionName = document.createElement('span');
2875
+ optionName.className = 'model-option-name';
2876
+ optionName.textContent = compactProfileLabel(profile);
2877
+ var optionMeta = document.createElement('span');
2878
+ optionMeta.className = 'model-option-meta';
2879
+ optionMeta.textContent = hasDuplicateModelName(profile) ? (profile.providerLabel || profile.provider || '') : '';
2880
+ optionMain.appendChild(optionName);
2881
+ optionMain.appendChild(optionMeta);
2882
+ var optionCheck = document.createElement('span');
2883
+ optionCheck.className = 'model-option-check';
2884
+ optionCheck.setAttribute('aria-hidden', 'true');
2885
+ option.appendChild(optionDot);
2886
+ option.appendChild(optionMain);
2887
+ option.appendChild(optionCheck);
2888
+ modelMenu.appendChild(option);
2889
+
2890
+ var row = document.createElement('div');
2891
+ row.className = 'profile-row' + (profile.id === activeProfileId ? ' active' : '') + (profile.connected ? ' connected' : '');
2892
+ row.title = fullProfileLabel(profile);
2893
+ row.addEventListener('click', function() {
2894
+ if (profile.id !== activeProfileId) vscode.postMessage({ type: 'selectProviderProfile', profileId: profile.id });
2895
+ });
2896
+
2897
+ var main = document.createElement('div');
2898
+ main.className = 'profile-main';
2899
+ var name = document.createElement('div');
2900
+ name.className = 'profile-name';
2901
+ name.textContent = fullProfileLabel(profile);
2902
+ var meta = document.createElement('div');
2903
+ meta.className = 'profile-meta';
2904
+ meta.textContent = profile.baseUrl || '';
2905
+ main.appendChild(name);
2906
+ main.appendChild(meta);
2907
+
2908
+ var actions = document.createElement('div');
2909
+ actions.className = 'profile-actions';
2910
+ var state = document.createElement('span');
2911
+ state.className = 'profile-state';
2912
+ state.title = profile.connected ? 'Connected' : 'Missing API key';
2913
+ var remove = document.createElement('button');
2914
+ remove.className = 'thread-delete';
2915
+ remove.title = 'Remove profile';
2916
+ remove.textContent = 'x';
2917
+ remove.addEventListener('click', function(event) {
2918
+ event.stopPropagation();
2919
+ vscode.postMessage({ type: 'removeProviderProfile', profileId: profile.id });
2920
+ });
2921
+ actions.appendChild(state);
2922
+ actions.appendChild(remove);
2923
+
2924
+ row.appendChild(main);
2925
+ row.appendChild(actions);
2926
+ profileList.appendChild(row);
2927
+ });
2928
+
2929
+ var activeProfile = providerProfiles.find(function(profile) { return profile.id === activeProfileId; });
2930
+ updateModelPicker(activeProfile);
2931
+ if (activeProfile) {
2932
+ syncProviderForm(activeProfile);
2933
+ }
2934
+ }
2935
+
2936
+ function setModelMenuOpen(open) {
2937
+ if (open) {
2938
+ var btnRect = modelPicker.getBoundingClientRect();
2939
+ // Place menu right above the button, aligned with its left edge
2940
+ modelMenu.style.bottom = (window.innerHeight - btnRect.top + 4) + 'px';
2941
+ modelMenu.style.left = Math.max(8, Math.min(btnRect.left, window.innerWidth - 300)) + 'px';
2942
+ modelMenu.style.top = 'auto';
2943
+ }
2944
+ modelMenu.classList.toggle('open', Boolean(open));
2945
+ modelPicker.setAttribute('aria-expanded', open ? 'true' : 'false');
2946
+ }
2947
+
2948
+ function updateModelPicker(profile) {
2949
+ modelPickerLabel.textContent = profile ? compactProfileLabel(profile) : 'No AI provider';
2950
+ modelPicker.title = profile ? fullProfileLabel(profile) : 'Active AI model';
2951
+ }
2952
+
2953
+ function fullProfileLabel(profile) {
2954
+ return (profile.providerLabel || profile.provider || 'AI') + (profile.model ? ' / ' + profile.model : '');
2955
+ }
2956
+
2957
+ function compactProfileLabel(profile) {
2958
+ var name = modelDisplayName(profile);
2959
+ return hasDuplicateModelName(profile) ? name + ' (' + (profile.providerLabel || profile.provider || 'AI') + ')' : name;
2960
+ }
2961
+
2962
+ function modelDisplayName(profile) {
2963
+ return profile.model || profile.providerLabel || profile.provider || 'AI provider';
2964
+ }
2965
+
2966
+ function modelDisplayKey(profile) {
2967
+ return modelDisplayName(profile).trim().toLowerCase();
2968
+ }
2969
+
2970
+ function hasDuplicateModelName(profile) {
2971
+ var key = modelDisplayKey(profile);
2972
+ if (!key) return false;
2973
+ return providerProfiles.filter(function(item) { return modelDisplayKey(item) === key; }).length > 1;
2974
+ }
2975
+
2976
+ function syncProviderForm(profile) {
2977
+ if (!profile) return;
2978
+ provider.value = profile.provider || provider.value;
2979
+ model.value = profile.model || '';
2980
+ baseUrl.value = profile.baseUrl || '';
2981
+ if (profile.connected && profile.requiresApiKey) {
2982
+ token.value = '';
2983
+ token.placeholder = 'API key is saved';
2984
+ token.classList.add('key-saved');
2985
+ } else {
2986
+ token.placeholder = 'API key';
2987
+ token.classList.remove('key-saved');
2988
+ }
2989
+ applyProviderDefaults(false);
2990
+ }
2991
+
2992
+ function renderModeMenu() {
2993
+ modeMenu.innerHTML = '';
2994
+ modeOptions.forEach(function(option) {
2995
+ var button = document.createElement('button');
2996
+ button.type = 'button';
2997
+ button.className = 'mode-option' + (option.id === currentMode ? ' active' : '');
2998
+ button.setAttribute('role', 'option');
2999
+ button.setAttribute('aria-selected', option.id === currentMode ? 'true' : 'false');
3000
+ button.addEventListener('click', function() {
3001
+ setModeMenuOpen(false);
3002
+ setMode(option.id);
3003
+ });
3004
+ var main = document.createElement('span');
3005
+ main.className = 'mode-option-main';
3006
+ var name = document.createElement('span');
3007
+ name.className = 'mode-option-name';
3008
+ name.textContent = option.label;
3009
+ var meta = document.createElement('span');
3010
+ meta.className = 'mode-option-meta';
3011
+ meta.textContent = option.hint;
3012
+ main.appendChild(name);
3013
+ main.appendChild(meta);
3014
+ var check = document.createElement('span');
3015
+ check.className = 'mode-option-check';
3016
+ check.setAttribute('aria-hidden', 'true');
3017
+ button.appendChild(main);
3018
+ button.appendChild(check);
3019
+ modeMenu.appendChild(button);
3020
+ });
3021
+ }
3022
+
3023
+ function setModeMenuOpen(open) {
3024
+ if (open) {
3025
+ var btnRect = modePicker.getBoundingClientRect();
3026
+ modeMenu.style.bottom = (window.innerHeight - btnRect.top + 4) + 'px';
3027
+ modeMenu.style.left = Math.max(8, Math.min(btnRect.left, window.innerWidth - 230)) + 'px';
3028
+ modeMenu.style.top = 'auto';
3029
+ }
3030
+ modeMenu.classList.toggle('open', Boolean(open));
3031
+ modePicker.setAttribute('aria-expanded', open ? 'true' : 'false');
3032
+ }
3033
+
3034
+ function setMode(mode) {
3035
+ currentMode = mode === 'auto' ? 'auto' : mode === 'chat' ? 'chat' : 'guide';
3036
+ var active = modeOptions.find(function(option) { return option.id === currentMode; }) || modeOptions[0];
3037
+ modePickerLabel.textContent = active.label;
3038
+ modePicker.title = active.label + ' - ' + active.hint;
3039
+ renderModeMenu();
3040
+ task.placeholder = currentMode === 'auto'
3041
+ ? 'Ask FlowSeeker for a reviewable plan...'
3042
+ : currentMode === 'chat'
3043
+ ? 'Chat directly with the AI provider...'
3044
+ : 'Ask FlowSeeker to find the relevant code...';
3045
+ }
3046
+
3047
+ applyProviderDefaults(true);
3048
+ renderModelDropdown([], model.value, provider.value);
3049
+ scheduleModelFetch();
3050
+
3051
+ /* Markdown renderer */
3052
+ function renderMarkdown(text) {
3053
+ if (!text) return '';
3054
+ var html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3055
+ html = html.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, function(_, lang, code) {
3056
+ return '<div class="code-block"><div class="code-header"><span class="lang">' + (lang || 'code') + '</span><button onclick="copyCode(this)">Copy</button></div><pre><code class="hljs">' + highlightCode(code) + '</code></pre></div>';
3057
+ });
3058
+ html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
3059
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
3060
+ html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
3061
+ html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
3062
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
3063
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
3064
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
3065
+ html = html.replace(/^(?:- |\\* )(.*)$/gm, '<li>$1</li>');
3066
+ html = html.replace(/((?:<li>.*<\\/li>\\n?)+)/g, '<ul>$1</ul>');
3067
+ html = html.replace(/^&gt;\\s?(.*)$/gm, '<blockquote>$1</blockquote>');
3068
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
3069
+ html = html.replace(/^---$/gm, '<hr>');
3070
+ html = html.replace(/([^\\n]+)\\n\\n/g, '<p>$1</p>\\n');
3071
+ return html;
3072
+ }
3073
+
3074
+ function highlightCode(code) {
3075
+ var result = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3076
+ result = result.replace(new RegExp('(//.*$|#.*$)', 'gm'), '<span class="hljs-comment">$1</span>');
3077
+ result = result.replace(/(["\`])((?:(?!\\1)[^\\\\]|\\\\.)*)\\1/g, '<span class="hljs-string">$1$2$1</span>');
3078
+ result = result.replace(/(')((?:(?!\\1)[^\\\\]|\\\\.)*)\\1/g, '<span class="hljs-string">$1$2$1</span>');
3079
+ result = result.replace(/\\b(\\d+\\.?\\d*)\\b/g, '<span class="hljs-number">$1</span>');
3080
+ var kw = ['function','const','let','var','return','if','else','for','while','class','extends','import','export','default','from','async','await','try','catch','throw','new','this','typeof','instanceof','public','private','protected','static','interface','type','enum','def','self','True','False','None','and','or','not','func','go','struct','map','chan','defer','package','fn','impl','use','mod','pub','continue','switch','case','break','yield','with','finally','get','set','namespace','declare','abstract','readonly','implements'];
3081
+ result = result.replace(new RegExp('\\\\b(' + kw.join('|') + ')\\\\b', 'g'), '<span class="hljs-keyword">$1</span>');
3082
+ result = result.replace(/\\b([a-zA-Z_$][\\w$]*)\\s*\\(/g, '<span class="hljs-function">$1</span>(');
3083
+ return result;
3084
+ }
3085
+
3086
+ window.copyCode = function(btn) {
3087
+ var code = btn.closest('.code-block').querySelector('code').textContent;
3088
+ navigator.clipboard.writeText(code).then(function() {
3089
+ btn.textContent = 'Copied!';
3090
+ btn.classList.add('copied');
3091
+ setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
3092
+ }).catch(function() {});
3093
+ };
3094
+
3095
+ function msgRow(kind) {
3096
+ var row = document.createElement('div');
3097
+ row.className = 'msg-row ' + kind;
3098
+ var avatar = document.createElement('div');
3099
+ avatar.className = 'msg-avatar';
3100
+ if (kind === 'user') avatar.textContent = 'U';
3101
+ else if (kind === 'error') avatar.textContent = '!';
3102
+ else avatar.innerHTML = '<img src="${logoUri}" alt="">';
3103
+ row.appendChild(avatar);
3104
+ var body = document.createElement('div');
3105
+ body.className = 'msg-body';
3106
+ var label = document.createElement('div');
3107
+ label.className = 'msg-label';
3108
+ label.textContent = kindLabel(kind);
3109
+ body.appendChild(label);
3110
+ row.appendChild(body);
3111
+ return row;
3112
+ }
3113
+
3114
+ function kindLabel(kind) {
3115
+ if (kind === 'user') return 'You';
3116
+ if (kind === 'retrieval') return 'Context';
3117
+ if (kind === 'status') return 'Status';
3118
+ if (kind === 'error') return 'Error';
3119
+ return 'FlowSeeker';
3120
+ }
3121
+
3122
+ function addMessage(kind, text, files, proposal, metric, feedback, verificationFiles, trace, commands, mcpTools) {
3123
+ removeEmpty();
3124
+ var row = msgRow(kind);
3125
+ var body = row.querySelector('.msg-body');
3126
+ var bubble = document.createElement('div');
3127
+ bubble.className = 'bubble';
3128
+
3129
+ var content = document.createElement('div');
3130
+ content.className = 'content';
3131
+ content.innerHTML = renderMarkdown(text || '');
3132
+ bubble.appendChild(content);
3133
+
3134
+ if (metric) {
3135
+ var card = document.createElement('div');
3136
+ var statusClass = metric.proofStatus || 'complete';
3137
+ card.className = 'metric-card ' + statusClass;
3138
+
3139
+ var statusLabel = statusClass === 'complete' ? 'Complete proof' : statusClass === 'partial' ? 'Partial proof' : 'Unavailable';
3140
+ var statusChip = '<span class="metric-status ' + statusClass + '">' + statusLabel + '</span>';
3141
+
3142
+ var coverageHtml = metric.contextCoverage ? '<div style="margin-top:4px;font-size:10px;color:var(--muted);">Coverage: ' + metric.contextCoverage + '</div>' : '';
3143
+
3144
+ var warningsHtml = '';
3145
+ if (metric.proofWarnings && metric.proofWarnings.length) {
3146
+ warningsHtml = '<div class="metric-warnings">' + metric.proofWarnings.map(function(w) { return '<div class="metric-warning">' + w + '</div>'; }).join('') + '</div>';
3147
+ }
3148
+
3149
+ card.innerHTML = '<div class="metric-header">Token Savings</div><div class="metric-main">' + metric.reduction + '</div><div class="metric-detail">Packet: ' + metric.packetTokens + ' | Workspace: ' + metric.workspaceTokens + ' | ' + metric.tokenizer + '</div>' + statusChip + coverageHtml + warningsHtml;
3150
+ bubble.appendChild(card);
3151
+ }
3152
+
3153
+ if (files && files.length) {
3154
+ var fl = document.createElement('div');
3155
+ fl.className = 'file-list';
3156
+ files.forEach(function(f) {
3157
+ var fr = document.createElement('div');
3158
+ fr.className = 'file-row';
3159
+ fr.title = f.path + ' - Click to open';
3160
+ fr.addEventListener('click', function() { vscode.postMessage({ type: 'openFile', file: f.path }); });
3161
+ fr.innerHTML = '<span class="path">' + f.path + '</span><span class="tag">' + f.role + '</span><span class="score">' + f.score + '</span>';
3162
+ fl.appendChild(fr);
3163
+ });
3164
+ bubble.appendChild(fl);
3165
+ }
3166
+
3167
+ if (feedback) {
3168
+ var fb = document.createElement('div');
3169
+ fb.className = 'feedback-bar';
3170
+ fb.innerHTML = '<span class="feedback-label">Was this helpful?</span>';
3171
+ [
3172
+ { k: 'relevant', t: 'Relevant' },
3173
+ { k: 'missing_file', t: 'Missing files' },
3174
+ { k: 'not_relevant', t: 'Irrelevant' },
3175
+ { k: 'agent_needed_more_context', t: 'AI needs more' }
3176
+ ].forEach(function(item) {
3177
+ var btn = document.createElement('button');
3178
+ btn.className = 'tiny ' + (item.k === 'relevant' ? 'positive' : 'secondary');
3179
+ btn.textContent = item.t;
3180
+ btn.addEventListener('click', function() { vscode.postMessage({ type: 'feedback', runId: feedback.runId, kind: item.k }); });
3181
+ fb.appendChild(btn);
3182
+ });
3183
+ bubble.appendChild(fb);
3184
+ }
3185
+
3186
+ if (trace && trace.length) {
3187
+ var traceList = document.createElement('div');
3188
+ traceList.className = 'trace-list';
3189
+ trace.forEach(function(item) {
3190
+ var row = document.createElement('div');
3191
+ row.className = 'trace-row';
3192
+ var dot = document.createElement('span');
3193
+ dot.className = 'trace-dot';
3194
+ var label = document.createElement('span');
3195
+ label.className = 'trace-text';
3196
+ label.textContent = item;
3197
+ row.appendChild(dot);
3198
+ row.appendChild(label);
3199
+ traceList.appendChild(row);
3200
+ });
3201
+ bubble.appendChild(traceList);
3202
+ }
3203
+
3204
+ if (proposal) {
3205
+ var prop = document.createElement('div');
3206
+ prop.style.cssText = 'margin-top:10px;';
3207
+ if (proposal.kind === 'plan') {
3208
+ var approveBtn = document.createElement('button');
3209
+ approveBtn.className = 'primary action-btn';
3210
+ approveBtn.textContent = 'Approve plan and generate edits';
3211
+ approveBtn.addEventListener('click', function() { vscode.postMessage({ type: 'approvePlan', planId: proposal.id }); });
3212
+ prop.appendChild(approveBtn);
3213
+ bubble.appendChild(prop);
3214
+ body.appendChild(bubble);
3215
+ messages.appendChild(row);
3216
+ messages.scrollTop = messages.scrollHeight;
3217
+ return;
3218
+ }
3219
+ if (proposal.edits && proposal.edits.length) {
3220
+ var list = document.createElement('div');
3221
+ list.style.cssText = 'display:flex;flex-direction:column;gap:3px;margin-bottom:8px;';
3222
+ proposal.edits.forEach(function(e) {
3223
+ var span = document.createElement('span');
3224
+ span.style.cssText = 'font-size:11px;font-family:var(--mono);color:var(--muted);';
3225
+ span.textContent = e.file + (e.range ? ' ' + e.range : '');
3226
+ list.appendChild(span);
3227
+ });
3228
+ prop.appendChild(list);
3229
+ }
3230
+ var diffBtn = document.createElement('button');
3231
+ diffBtn.className = 'secondary action-btn';
3232
+ diffBtn.textContent = 'Open diff';
3233
+ diffBtn.addEventListener('click', function() { vscode.postMessage({ type: 'openDiff', proposalId: proposal.id }); });
3234
+ prop.appendChild(diffBtn);
3235
+ var applyBtn = document.createElement('button');
3236
+ applyBtn.className = 'primary action-btn';
3237
+ applyBtn.textContent = 'Apply proposed edits';
3238
+ applyBtn.addEventListener('click', function() { vscode.postMessage({ type: 'applyEdits', proposalId: proposal.id }); });
3239
+ prop.appendChild(applyBtn);
3240
+ bubble.appendChild(prop);
3241
+ }
3242
+
3243
+ if (commands && commands.length) {
3244
+ var commandList = document.createElement('div');
3245
+ commandList.className = 'command-list';
3246
+ commands.forEach(function(cmd) {
3247
+ var row = document.createElement('div');
3248
+ row.className = 'command-row';
3249
+ var main = document.createElement('div');
3250
+ main.className = 'command-main';
3251
+ var textEl = document.createElement('span');
3252
+ textEl.className = 'command-text';
3253
+ textEl.textContent = cmd.command || '';
3254
+ main.appendChild(textEl);
3255
+ if (cmd.rationale) {
3256
+ var reason = document.createElement('span');
3257
+ reason.className = 'command-reason';
3258
+ reason.textContent = cmd.rationale;
3259
+ main.appendChild(reason);
3260
+ }
3261
+ var runBtn = document.createElement('button');
3262
+ runBtn.className = 'secondary tiny action-btn';
3263
+ runBtn.textContent = 'Run';
3264
+ runBtn.addEventListener('click', function() {
3265
+ vscode.postMessage({ type: 'runAgentCommand', command: cmd });
3266
+ });
3267
+ row.appendChild(main);
3268
+ row.appendChild(runBtn);
3269
+ commandList.appendChild(row);
3270
+ });
3271
+ bubble.appendChild(commandList);
3272
+ }
3273
+
3274
+ if (mcpTools && mcpTools.length) {
3275
+ var toolList = document.createElement('div');
3276
+ toolList.className = 'command-list';
3277
+ mcpTools.forEach(function(tool) {
3278
+ var row = document.createElement('div');
3279
+ row.className = 'command-row';
3280
+ var main = document.createElement('div');
3281
+ main.className = 'command-main';
3282
+ var textEl = document.createElement('span');
3283
+ textEl.className = 'command-text';
3284
+ textEl.textContent = tool.tool || '';
3285
+ main.appendChild(textEl);
3286
+ if (tool.rationale) {
3287
+ var reason = document.createElement('span');
3288
+ reason.className = 'command-reason';
3289
+ reason.textContent = tool.rationale;
3290
+ main.appendChild(reason);
3291
+ }
3292
+ var runBtn = document.createElement('button');
3293
+ runBtn.className = 'secondary tiny action-btn';
3294
+ runBtn.textContent = 'Invoke';
3295
+ runBtn.addEventListener('click', function() {
3296
+ vscode.postMessage({ type: 'runMcpTool', tool: tool });
3297
+ });
3298
+ row.appendChild(main);
3299
+ row.appendChild(runBtn);
3300
+ toolList.appendChild(row);
3301
+ });
3302
+ bubble.appendChild(toolList);
3303
+ }
3304
+
3305
+ if (verificationFiles && verificationFiles.length) {
3306
+ var verify = document.createElement('div');
3307
+ verify.style.cssText = 'margin-top:10px;';
3308
+ var verifyBtn = document.createElement('button');
3309
+ verifyBtn.className = 'secondary action-btn';
3310
+ verifyBtn.textContent = 'Verify changes';
3311
+ verifyBtn.addEventListener('click', function() {
3312
+ vscode.postMessage({ type: 'verifyFiles', files: verificationFiles });
3313
+ });
3314
+ verify.appendChild(verifyBtn);
3315
+ bubble.appendChild(verify);
3316
+ }
3317
+
3318
+ // Copy button (visible on hover)
3319
+ var actions = document.createElement('div');
3320
+ actions.className = 'msg-actions';
3321
+ var copyBtn = document.createElement('button');
3322
+ copyBtn.className = 'secondary tiny';
3323
+ copyBtn.textContent = 'Copy';
3324
+ copyBtn.addEventListener('click', function() {
3325
+ navigator.clipboard.writeText(text || '').catch(function() {});
3326
+ copyBtn.textContent = 'Copied!';
3327
+ setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
3328
+ });
3329
+ actions.appendChild(copyBtn);
3330
+ if (metric) {
3331
+ var proofBtn = document.createElement('button');
3332
+ proofBtn.className = 'secondary tiny';
3333
+ proofBtn.textContent = 'Copy proof';
3334
+ proofBtn.title = 'Copy token proof summary';
3335
+ proofBtn.addEventListener('click', function() { vscode.postMessage({ type: 'copyTokenProof' }); });
3336
+ actions.appendChild(proofBtn);
3337
+ }
3338
+ bubble.appendChild(actions);
3339
+
3340
+ body.appendChild(bubble);
3341
+ messages.appendChild(row);
3342
+ messages.scrollTop = messages.scrollHeight;
3343
+ }
3344
+
3345
+ function startStreamingMessage() {
3346
+ removeStreamingMessage();
3347
+ removeEmpty();
3348
+ activeStreamingText = '';
3349
+ activeStreamingRow = msgRow('assistant');
3350
+ var body = activeStreamingRow.querySelector('.msg-body');
3351
+ var bubble = document.createElement('div');
3352
+ bubble.className = 'bubble';
3353
+ activeStreamingContent = document.createElement('div');
3354
+ activeStreamingContent.className = 'content';
3355
+ bubble.appendChild(activeStreamingContent);
3356
+ body.appendChild(bubble);
3357
+ messages.appendChild(activeStreamingRow);
3358
+ messages.scrollTop = messages.scrollHeight;
3359
+ }
3360
+
3361
+ function appendStreamingMessage(text) {
3362
+ if (!activeStreamingRow) startStreamingMessage();
3363
+ activeStreamingText += text || '';
3364
+ activeStreamingContent.innerHTML = renderMarkdown(activeStreamingText);
3365
+ messages.scrollTop = messages.scrollHeight;
3366
+ }
3367
+
3368
+ function removeStreamingMessage() {
3369
+ if (activeStreamingRow && activeStreamingRow.parentElement) {
3370
+ activeStreamingRow.parentElement.removeChild(activeStreamingRow);
3371
+ }
3372
+ activeStreamingRow = null;
3373
+ activeStreamingContent = null;
3374
+ activeStreamingText = '';
3375
+ }
3376
+
3377
+ function removeEmpty() {
3378
+ var e = document.getElementById('empty');
3379
+ if (e) { e.style.display = 'none'; }
3380
+ }
3381
+
3382
+ function clearMessages() {
3383
+ messages.innerHTML = '';
3384
+ var empty = document.createElement('div');
3385
+ empty.className = 'empty-state';
3386
+ empty.id = 'empty';
3387
+ empty.innerHTML = '<div class="brand-lg"><img src="${logoUri}" alt=""></div><div class="empty-title"><span class="wordmark" data-text="FlowSeeker">FlowSeeker</span></div><div class="empty-hint">New session</div>';
3388
+ messages.appendChild(empty);
3389
+ }
3390
+
3391
+ function renderMessages(historyMessages) {
3392
+ clearMessages();
3393
+ if (!historyMessages || !historyMessages.length) return;
3394
+ historyMessages.forEach(function(msg) {
3395
+ addMessage(msg.kind || msg.type || 'assistant', msg.text, msg.files, msg.proposal, msg.metric, msg.feedback, msg.verificationFiles, msg.trace, msg.commands, msg.mcpTools);
3396
+ });
3397
+ }
3398
+
3399
+ function renderThreadList(threads, activeId) {
3400
+ activeThreadId = activeId || activeThreadId;
3401
+ threadList.innerHTML = '';
3402
+ (threads || []).forEach(function(thread) {
3403
+ var row = document.createElement('div');
3404
+ row.className = 'thread-row' + (thread.id === activeThreadId ? ' active' : '');
3405
+ row.title = thread.title || 'New chat';
3406
+ row.addEventListener('click', function() {
3407
+ if (thread.id !== activeThreadId && !busy) vscode.postMessage({ type: 'selectThread', threadId: thread.id });
3408
+ });
3409
+
3410
+ var main = document.createElement('div');
3411
+ main.className = 'thread-main';
3412
+ var title = document.createElement('div');
3413
+ title.className = 'thread-title';
3414
+ title.textContent = thread.title || 'New chat';
3415
+ var meta = document.createElement('div');
3416
+ meta.className = 'thread-meta';
3417
+ meta.textContent = formatThreadTime(thread.updatedAt) + ' - ' + (thread.messageCount || 0) + ' msg';
3418
+ main.appendChild(title);
3419
+ main.appendChild(meta);
3420
+
3421
+ var del = document.createElement('button');
3422
+ del.className = 'thread-delete';
3423
+ del.title = 'Delete chat';
3424
+ del.textContent = 'x';
3425
+ del.addEventListener('click', function(event) {
3426
+ event.stopPropagation();
3427
+ if (!busy) vscode.postMessage({ type: 'deleteThread', threadId: thread.id });
3428
+ });
3429
+
3430
+ row.appendChild(main);
3431
+ row.appendChild(del);
3432
+ threadList.appendChild(row);
3433
+ });
3434
+ }
3435
+
3436
+ function renderQuotaPanel(usage, profiles, quotaDebug) {
3437
+ var panel = document.getElementById('quotaPanel');
3438
+ var summary = document.getElementById('quotaSummaryText');
3439
+ var badge = document.getElementById('quotaBadge');
3440
+ var detail = document.getElementById('quotaDetail');
3441
+
3442
+ if (!panel || !summary || !badge || !detail) return;
3443
+
3444
+ // Debug: show total record count from backend
3445
+ var debugInfo = (typeof quotaDebug === 'number') ? ' [' + quotaDebug + ' records]' : '';
3446
+
3447
+ // Build model list from usage data + profiles
3448
+ var models = [];
3449
+ var seen = {};
3450
+ if (usage && usage.models && usage.models.length) {
3451
+ usage.models.forEach(function(m) {
3452
+ var k = m.provider + '|' + m.model;
3453
+ seen[k] = true;
3454
+ models.push(m);
3455
+ });
3456
+ }
3457
+ if (profiles && profiles.length) {
3458
+ var now = Date.now();
3459
+ var tmr = new Date(); tmr.setDate(tmr.getDate() + 1); tmr.setHours(0,0,0,0);
3460
+ profiles.forEach(function(p) {
3461
+ var k = p.provider + '|' + p.model;
3462
+ if (!seen[k]) {
3463
+ seen[k] = true;
3464
+ models.push({
3465
+ provider: p.provider, model: p.model,
3466
+ dailyTokensUsed: -1, dailyTokenLimit: -1, dailyPct: -1,
3467
+ quotaSource: 'unknown',
3468
+ monthlyCostUsed: -1, monthlyCostLimit: -1, monthlyPct: -1,
3469
+ requestsToday: 0, dailyResetMs: tmr.getTime(), monthlyResetMs: tmr.getTime()
3470
+ });
3471
+ }
3472
+ });
3473
+ }
3474
+
3475
+ if (!models.length) { panel.classList.remove('open'); return; }
3476
+ panel.classList.add('open');
3477
+
3478
+ var hasAnyUsage = models.some(function(m) {
3479
+ return m.dailyTokensUsed >= 0 || m.quotaSource === 'live' ||
3480
+ typeof m.requestsLimit === 'number' || typeof m.requestsRemaining === 'number';
3481
+ });
3482
+ var maxPct = 0;
3483
+ var totalTodayTokens = 0;
3484
+ models.forEach(function(m) {
3485
+ var requestPct = requestQuotaPct(m);
3486
+ var pct = m.dailyPct >= 0 ? m.dailyPct : requestPct;
3487
+ if (pct > maxPct) maxPct = pct;
3488
+ if (m.dailyTokensUsed > 0) totalTodayTokens += m.dailyTokensUsed;
3489
+ });
3490
+
3491
+ if (hasAnyUsage) {
3492
+ var badgeClass = maxPct >= 80 ? 'danger' : maxPct >= 50 ? 'warn' : 'ok';
3493
+ badge.textContent = maxPct + '%';
3494
+ badge.className = 'quota-badge ' + badgeClass;
3495
+ summary.textContent = 'Quota: ' + formatTokenCount(totalTodayTokens) + ' today / $' + (usage ? usage.todayCostUSD.toFixed(2) : '0.00') + ' today - click to expand' + debugInfo;
3496
+ } else {
3497
+ badge.textContent = '?';
3498
+ badge.className = 'quota-badge ok';
3499
+ summary.textContent = 'Quota: fetch from next request - click to expand' + debugInfo;
3500
+ }
3501
+
3502
+ var detailHtml = '';
3503
+ models.forEach(function(m) {
3504
+ var hasTokenData = m.dailyTokensUsed >= 0;
3505
+ var hasTokenLimit = m.dailyTokenLimit > 0;
3506
+ var hasRequestLimit = typeof m.requestsLimit === 'number' && m.requestsLimit >= 0;
3507
+ var hasRequestRemaining = typeof m.requestsRemaining === 'number' && m.requestsRemaining >= 0;
3508
+ var hasData = hasTokenData || hasRequestLimit || hasRequestRemaining || m.quotaSource === 'live';
3509
+ var requestPct = requestQuotaPct(m);
3510
+ var dailyPct = hasTokenLimit && m.dailyPct >= 0 ? m.dailyPct : requestPct;
3511
+ if (dailyPct < 0) dailyPct = 0;
3512
+ var barClass = dailyPct >= 80 ? 'high' : dailyPct >= 50 ? 'mid' : 'low';
3513
+ var resetCountdown = formatCountdown((m.dailyResetMs || Date.now() + 86400000) - Date.now());
3514
+ var statusLabel = m.quotaSource === 'live' ? 'Live' : m.quotaSource === 'manual' ? 'Manual' : m.quotaSource === 'default' ? 'Default' : 'Unknown';
3515
+ var statusColor = hasData ? 'var(--vscode-foreground)' : 'var(--muted)';
3516
+ var tokenText = hasTokenData
3517
+ ? formatTokenCount(m.dailyTokensUsed) + (hasTokenLimit ? ' / ' + formatTokenCount(m.dailyTokenLimit) : '') + ' tokens today'
3518
+ : 'Token limit unknown';
3519
+ var requestText = requestQuotaText(m);
3520
+ var costText = m.monthlyCostUsed >= 0
3521
+ ? '$' + (m.monthlyCostUsed||0).toFixed(2) + ' / $' + (m.monthlyCostLimit||10) + ' this month'
3522
+ : 'cost unknown';
3523
+ detailHtml +=
3524
+ '<div class="quota-row">' +
3525
+ '<span class="quota-model-name">' + (m.provider || '') + ' / ' + (m.model || '') + '</span>' +
3526
+ '<span style="font-weight:700;font-size:11px;color:' + statusColor + '">' + (hasData && dailyPct > 0 ? dailyPct + '%' : statusLabel) + '</span>' +
3527
+ '<div class="quota-bar-wrap"><div class="quota-bar-fill ' + barClass + '" style="width:' + dailyPct + '%"></div></div>' +
3528
+ '<span class="quota-stats">' +
3529
+ (hasData
3530
+ ? tokenText + ' | ' + requestText + ' | ' + costText + ' | '
3531
+ : 'Will update after next API request | ') +
3532
+ 'resets in ' + resetCountdown +
3533
+ '</span>' +
3534
+ '</div>';
3535
+ });
3536
+ detail.innerHTML = detailHtml;
3537
+ }
3538
+
3539
+ function requestQuotaPct(m) {
3540
+ if (typeof m.requestsLimit !== 'number' || m.requestsLimit <= 0 || typeof m.requestsRemaining !== 'number') return -1;
3541
+ var used = Math.max(0, m.requestsLimit - m.requestsRemaining);
3542
+ return Math.min(100, Math.round((used / m.requestsLimit) * 100));
3543
+ }
3544
+
3545
+ function requestQuotaText(m) {
3546
+ var hasLimit = typeof m.requestsLimit === 'number' && m.requestsLimit >= 0;
3547
+ var hasRemaining = typeof m.requestsRemaining === 'number' && m.requestsRemaining >= 0;
3548
+ if (hasLimit && hasRemaining) {
3549
+ return formatTokenCount(Math.max(0, m.requestsLimit - m.requestsRemaining)) + ' / ' + formatTokenCount(m.requestsLimit) + ' provider reqs';
3550
+ }
3551
+ if (hasRemaining) return formatTokenCount(m.requestsRemaining) + ' provider reqs remaining';
3552
+ if (hasLimit) return formatTokenCount(m.requestsLimit) + ' provider req limit';
3553
+ return (m.requestsToday || 0) + ' reqs';
3554
+ }
3555
+
3556
+ function formatTokenCount(n) {
3557
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
3558
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
3559
+ return String(n);
3560
+ }
3561
+
3562
+ function formatCountdown(ms) {
3563
+ if (ms <= 0) return 'soon';
3564
+ var h = Math.floor(ms / 3600000);
3565
+ var m = Math.floor((ms % 3600000) / 60000);
3566
+ if (h > 0) return h + 'h ' + m + 'm';
3567
+ return m + 'm';
3568
+ }
3569
+
3570
+ function renderModelDropdown(models, activeModelId, providerId) {
3571
+ var sel = document.getElementById('oauthModelSelect');
3572
+ if (!sel) return;
3573
+ providerModelOptions = Array.isArray(models) ? models : [];
3574
+ providerModelOptionsProvider = providerId || provider.value || '';
3575
+ sel.innerHTML = '';
3576
+ if (!providerModelOptions.length) {
3577
+ var placeholder = document.createElement('option');
3578
+ placeholder.value = '';
3579
+ placeholder.textContent = providerMeta[providerModelOptionsProvider]?.requiresApiKey ? 'Enter API key to load models' : 'No models found';
3580
+ placeholder.disabled = true;
3581
+ placeholder.selected = true;
3582
+ sel.appendChild(placeholder);
3583
+ }
3584
+ providerModelOptions.forEach(function(m) {
3585
+ var opt = document.createElement('option');
3586
+ opt.value = m.id;
3587
+ opt.textContent = m.name || m.id;
3588
+ if (m.id === activeModelId) opt.selected = true;
3589
+ sel.appendChild(opt);
3590
+ });
3591
+ if (activeModelId && !providerModelOptions.some(function(m) { return m.id === activeModelId; })) {
3592
+ var fallback = document.createElement('option');
3593
+ fallback.value = activeModelId;
3594
+ fallback.textContent = activeModelId;
3595
+ fallback.selected = true;
3596
+ sel.insertBefore(fallback, sel.firstChild);
3597
+ }
3598
+ applyProviderDefaults(false);
3599
+ }
3600
+
3601
+ function renderAgentPolicy(policy) {
3602
+ policy = policy || {};
3603
+ var parts = [];
3604
+ parts.push(policy.editWorkspace ? 'auto edits on' : 'review edits');
3605
+ if (policy.allCommands) parts.push('commands auto');
3606
+ else if (policy.safeCommands) parts.push('safe checks auto');
3607
+ else parts.push('manual checks');
3608
+ if (policy.mcpTools) parts.push('MCP auto');
3609
+ if (policy.mcpServerCount) parts.push(policy.mcpServerCount + ' MCP');
3610
+ if (policy.rulesCount) parts.push(policy.rulesCount + ' rule' + (policy.rulesCount === 1 ? '' : 's'));
3611
+ agentPolicyText.textContent = parts.join(' · ');
3612
+
3613
+ var mcpServerLines = ['MCP servers:'];
3614
+ if (policy.mcpServers && policy.mcpServers.length) {
3615
+ policy.mcpServers.forEach(function(s) {
3616
+ var status = !s.enabled ? 'disabled' : s.valid ? 'configured' : 'misconfigured';
3617
+ mcpServerLines.push(' ' + s.name + ' (' + s.type + ') - ' + status);
3618
+ });
3619
+ } else {
3620
+ mcpServerLines.push(' none configured');
3621
+ }
3622
+ agentPolicy.title = [
3623
+ 'Workspace reads: ' + (policy.readWorkspace ? 'allowed' : 'ask first'),
3624
+ 'Workspace edits: ' + (policy.editWorkspace ? 'auto-approved' : 'review before apply'),
3625
+ 'Safe commands: ' + (policy.safeCommands ? 'auto-approved' : 'manual'),
3626
+ 'All commands: ' + (policy.allCommands ? 'auto-approved' : 'manual'),
3627
+ 'MCP tools: ' + (policy.mcpTools ? 'auto-approved' : 'manual'),
3628
+ ''
3629
+ ].concat(mcpServerLines).concat([
3630
+ '',
3631
+ 'Rules: ' + (policy.rulesCount || 0)
3632
+ ]).join('\\n');
3633
+ agentPolicy.classList.toggle('relaxed', Boolean(policy.editWorkspace || policy.safeCommands || policy.mcpTools));
3634
+ agentPolicy.classList.toggle('risky', Boolean(policy.allCommands));
3635
+ if (policyReadWorkspace) policyReadWorkspace.checked = Boolean(policy.readWorkspace);
3636
+ if (policyEditWorkspace) policyEditWorkspace.checked = Boolean(policy.editWorkspace);
3637
+ if (policySafeCommands) policySafeCommands.checked = Boolean(policy.safeCommands);
3638
+ if (policyAllCommands) policyAllCommands.checked = Boolean(policy.allCommands);
3639
+ if (policyMcpTools) policyMcpTools.checked = Boolean(policy.mcpTools);
3640
+ if (agentRules && document.activeElement !== agentRules) {
3641
+ agentRules.value = Array.isArray(policy.rules) ? policy.rules.join('\\n') : '';
3642
+ }
3643
+ }
3644
+
3645
+ function formatThreadTime(value) {
3646
+ if (!value) return 'Now';
3647
+ var date = new Date(value);
3648
+ if (Number.isNaN(date.getTime())) return 'Now';
3649
+ return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
3650
+ }
3651
+
3652
+ function setAgentState(state) {
3653
+ agentState = state;
3654
+ var isBusy = state !== 'idle' && state !== 'cancelled' && state !== 'retrieval_complete';
3655
+ busy = isBusy;
3656
+ sendButton.style.display = isBusy ? 'none' : '';
3657
+ stopBtn.style.display = isBusy ? '' : 'none';
3658
+ task.readOnly = isBusy;
3659
+ task.style.opacity = isBusy ? '.55' : '1';
3660
+ if (!isBusy) task.focus();
3661
+ // Disable action buttons on past messages when busy with another action
3662
+ var actionBtns = messages.querySelectorAll('.action-btn');
3663
+ actionBtns.forEach(function(btn) { btn.disabled = isBusy; });
3664
+ }
3665
+
3666
+ window.addEventListener('message', function(event) {
3667
+ var msg = event.data;
3668
+ if (msg.type === 'connection') {
3669
+ renderProviderProfiles(msg.profiles || [], msg.activeProfileId || msg.profileId || '');
3670
+ var usageText = '';
3671
+ if (msg.usage && msg.usage.totalCostUSD > 0) {
3672
+ usageText = ' | Today $' + msg.usage.todayCostUSD.toFixed(3) + ' | Total $' + msg.usage.totalCostUSD.toFixed(3) + ' (' + msg.usage.totalRequests + ' reqs)';
3673
+ }
3674
+ statusBadge.textContent = msg.connected ? 'Connected' : 'Disconnected';
3675
+ connectionStatusText.textContent = msg.connected
3676
+ ? 'Ready - ' + (msg.providerLabel || msg.provider || 'AI provider') + usageText
3677
+ : 'Local retrieval only' + usageText;
3678
+ connection.classList.toggle('connected', Boolean(msg.connected));
3679
+ renderGatewayConnectionStatus(msg.connected, msg.providerLabel || msg.provider || '', msg.model || '');
3680
+ renderQuotaPanel(msg.usage, msg.profiles, msg.quotaDebug);
3681
+ var activeProfile = providerProfiles.find(function(profile) { return profile.id === activeProfileId; });
3682
+ var activeProviderForModels = activeProfile ? activeProfile.provider : msg.provider;
3683
+ var activeModelForModels = activeProfile ? activeProfile.model : msg.model;
3684
+ renderModelDropdown(msg.models || [], activeModelForModels || '', activeProviderForModels || '');
3685
+ if (activeProfile) {
3686
+ syncProviderForm(activeProfile);
3687
+ } else {
3688
+ if (msg.provider) provider.value = msg.provider;
3689
+ if (msg.model) model.value = msg.model;
3690
+ if (msg.baseUrl) baseUrl.value = msg.baseUrl;
3691
+ applyProviderDefaults(false);
3692
+ }
3693
+ return;
3694
+ }
3695
+ if (msg.type === 'gatewayFeedback') {
3696
+ showGatewayFeedback(msg.status || 'info', msg.text || '', true);
3697
+ return;
3698
+ }
3699
+ if (msg.type === 'agentPolicy') {
3700
+ renderAgentPolicy(msg);
3701
+ return;
3702
+ }
3703
+ if (msg.type === 'models') {
3704
+ if (!msg.provider || msg.provider === provider.value) {
3705
+ renderModelDropdown(msg.models || [], msg.model || model.value || '', msg.provider || provider.value);
3706
+ }
3707
+ return;
3708
+ }
3709
+ if (msg.type === 'threadState') {
3710
+ renderThreadList(msg.threads || [], msg.activeThreadId || '');
3711
+ renderMessages(msg.messages || []);
3712
+ setAgentState('idle');
3713
+ return;
3714
+ }
3715
+ if (msg.type === 'threads') {
3716
+ renderThreadList(msg.threads || [], msg.activeThreadId || '');
3717
+ return;
3718
+ }
3719
+ if (msg.type === 'assistantStart') {
3720
+ startStreamingMessage();
3721
+ setAgentState('asking_provider');
3722
+ return;
3723
+ }
3724
+ if (msg.type === 'assistantDelta') {
3725
+ appendStreamingMessage(msg.text || '');
3726
+ setAgentState('asking_provider');
3727
+ return;
3728
+ }
3729
+ if (msg.type === 'retrieval') {
3730
+ addMessage('retrieval', msg.text, msg.files || [], msg.proposal, msg.metric, msg.feedback, msg.verificationFiles, msg.trace, msg.commands, msg.mcpTools);
3731
+ setAgentState('retrieval_complete');
3732
+ return;
3733
+ }
3734
+ if (msg.type === 'status') {
3735
+ addMessage('status', msg.text, void 0, void 0, void 0, void 0, msg.verificationFiles, msg.trace, msg.commands, msg.mcpTools);
3736
+ var statusText = (msg.text || '').toLowerCase();
3737
+ if (statusText.indexOf('cancelled') >= 0) { removeStreamingMessage(); setAgentState('cancelled'); }
3738
+ else if (statusText.indexOf('verifying') >= 0) setAgentState('verifying');
3739
+ else if (statusText.indexOf('applying') >= 0) setAgentState('applying_edits');
3740
+ else if (statusText.indexOf('finding') >= 0 || statusText.indexOf('retriev') >= 0) setAgentState('retrieving');
3741
+ else if (statusText.indexOf('asking') >= 0) setAgentState('asking_provider');
3742
+ return;
3743
+ }
3744
+ if (msg.type === 'assistant' || msg.type === 'user' || msg.type === 'error') {
3745
+ if (msg.type === 'assistant' || msg.type === 'error') removeStreamingMessage();
3746
+ addMessage(msg.type, msg.text, void 0, msg.proposal, msg.metric, msg.feedback, msg.verificationFiles, msg.trace, msg.commands, msg.mcpTools);
3747
+ setAgentState('idle');
3748
+ }
3749
+ });
3750
+
3751
+ function send() {
3752
+ var cleanTask = task.value.trim();
3753
+ if (!cleanTask || busy) return;
3754
+ setAgentState('retrieving');
3755
+ vscode.postMessage({ type: 'ask', task: cleanTask, mode: currentMode });
3756
+ task.value = '';
3757
+ }
3758
+
3759
+ vscode.postMessage({ type: 'ready' });
3760
+ </script>
3761
+ </body>
3762
+ </html>`;
3763
+ }
3764
+ }
3765
+ exports.ChatViewProvider = ChatViewProvider;
3766
+ ChatViewProvider.viewType = "flowseeker.chatView";
3767
+ function formatFileSummary(result) {
3768
+ const groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units).slice(0, 8);
3769
+ const stats = result.stats;
3770
+ return [
3771
+ `FlowSeeker retrieved ${result.units.length} evidence units from ${stats.scannedFiles} scanned files.`,
3772
+ result.contextCoverage
3773
+ ? `Context coverage: ${result.contextCoverage.foundRequiredSlots.length}/${result.contextCoverage.requiredSlots.length} required slots${result.contextCoverage.missingRequiredSlots.length ? `; missing ${result.contextCoverage.missingRequiredSlots.join(", ")}` : ""}.`
3774
+ : "",
3775
+ `Intent: ${result.profile.intent}`,
3776
+ `Keywords: ${result.profile.keywords.join(", ") || "none"}`,
3777
+ "",
3778
+ "Top files:",
3779
+ ...groups.map((group, index) => `${index + 1}. ${group.relativePath} [${group.role}/${group.slot}] score=${group.score.toFixed(1)} evidence=${group.unitCount}`)
3780
+ ].join("\n");
3781
+ }
3782
+ function formatTokenMetric(result) {
3783
+ const tokenSavings = result.tokenSavings;
3784
+ if (!tokenSavings || tokenSavings.status === "unavailable") {
3785
+ return undefined;
3786
+ }
3787
+ return {
3788
+ packetTokens: (0, tokenSavings_1.formatTokenCount)(tokenSavings.solvePacketTokensEstimate),
3789
+ workspaceTokens: (0, tokenSavings_1.formatTokenCount)(tokenSavings.workspaceTokensEstimate),
3790
+ reduction: (0, tokenSavings_1.formatReductionPercent)(tokenSavings.reductionPercent),
3791
+ tokenizer: tokenSavings.tokenizer,
3792
+ proofStatus: tokenSavings.proofStatus,
3793
+ proofWarnings: tokenSavings.proofWarnings ?? [],
3794
+ contextCoverage: result.contextCoverage
3795
+ ? `${result.contextCoverage.foundRequiredSlots.length}/${result.contextCoverage.requiredSlots.length} slots`
3796
+ : ""
3797
+ };
3798
+ }
3799
+ function createThread() {
3800
+ const now = new Date().toISOString();
3801
+ return {
3802
+ id: createThreadId(),
3803
+ title: "New chat",
3804
+ createdAt: now,
3805
+ updatedAt: now,
3806
+ messages: []
3807
+ };
3808
+ }
3809
+ function createThreadId() {
3810
+ return `thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3811
+ }
3812
+ function titleFromMessage(text) {
3813
+ const collapsed = text.replace(/\s+/g, " ").trim();
3814
+ if (!collapsed) {
3815
+ return "New chat";
3816
+ }
3817
+ return collapsed.length > 46 ? `${collapsed.slice(0, 43)}...` : collapsed;
3818
+ }
3819
+ function normalizeThreads(threads) {
3820
+ const now = new Date().toISOString();
3821
+ return threads
3822
+ .filter((thread) => thread && typeof thread.id === "string")
3823
+ .map((thread) => ({
3824
+ id: thread.id,
3825
+ title: thread.title || titleFromMessage(thread.messages?.find((message) => message.kind === "user")?.text ?? "New chat"),
3826
+ createdAt: thread.createdAt || now,
3827
+ updatedAt: thread.updatedAt || thread.createdAt || now,
3828
+ messages: Array.isArray(thread.messages) ? thread.messages.slice(-maxHistoryMessages) : []
3829
+ }))
3830
+ .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
3831
+ }
3832
+ function isCancellationError(error) {
3833
+ return error instanceof Error && /^cancelled$/i.test(error.message.trim());
3834
+ }
3835
+ function getNonce() {
3836
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
3837
+ let nonce = "";
3838
+ for (let index = 0; index < 32; index += 1) {
3839
+ nonce += chars.charAt(Math.floor(Math.random() * chars.length));
3840
+ }
3841
+ return nonce;
3842
+ }
3843
+ function escapeHtml(value) {
3844
+ return value
3845
+ .replace(/&/g, "&amp;")
3846
+ .replace(/</g, "&lt;")
3847
+ .replace(/>/g, "&gt;")
3848
+ .replace(/"/g, "&quot;");
3849
+ }
3850
+ function toProviderInput(message) {
3851
+ const provider = message.provider ?? "customGateway";
3852
+ return {
3853
+ provider,
3854
+ model: message.model,
3855
+ baseUrl: message.baseUrl ?? message.endpoint,
3856
+ apiKey: message.token
3857
+ };
3858
+ }
3859
+ function formatErrorMessage(error) {
3860
+ return error instanceof Error ? error.message : String(error);
3861
+ }
3862
+ function shouldProbeQuotaOnConnect(provider) {
3863
+ return provider !== "codexCli" && provider !== "claudeCode" && provider !== "ollama";
3864
+ }
3865
+ function formatQuotaProbeWarning(error) {
3866
+ const message = error instanceof Error ? error.message : String(error);
3867
+ if (/429/.test(message) && /quota|billing/i.test(message)) {
3868
+ return "The API key is valid enough to save and fetch models, but the provider rejected text generation with quota/billing exhausted. Add billing, raise the project budget, or use another key/project before asking FlowSeeker to call the model.";
3869
+ }
3870
+ if (/429/.test(message)) {
3871
+ return "The provider rate-limited the test request. The profile was saved; try again later or use another model/key.";
3872
+ }
3873
+ return message;
3874
+ }
3875
+ function renderHistoryMessage(message) {
3876
+ const state = message.kind === "status" && /cancelled/i.test(message.text) ? "cancelled"
3877
+ : message.kind === "status" && /verifying/i.test(message.text) ? "verifying"
3878
+ : message.kind === "status" && /applying/i.test(message.text) ? "applying_edits"
3879
+ : message.kind === "status" && /finding/i.test(message.text) ? "retrieving"
3880
+ : message.kind === "status" && /asking/i.test(message.text) ? "asking_provider"
3881
+ : message.kind === "error" ? "error"
3882
+ : message.kind === "assistant" || message.kind === "user" ? "idle"
3883
+ : undefined;
3884
+ return {
3885
+ type: message.kind === "retrieval" ? "retrieval" : message.kind,
3886
+ kind: message.kind,
3887
+ text: message.text,
3888
+ state,
3889
+ trace: message.trace,
3890
+ commands: message.commands,
3891
+ mcpTools: message.mcpTools,
3892
+ files: message.files,
3893
+ metric: message.metric,
3894
+ feedback: message.feedback,
3895
+ proposal: message.proposal,
3896
+ verificationFiles: message.verificationFiles
3897
+ };
3898
+ }
3899
+ //# sourceMappingURL=chatViewProvider.js.map