@64mb/freelens-power 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +7 -4
  2. package/out/main/ai-ipc.js +34 -3
  3. package/out/main/ai-ipc.js.map +1 -1
  4. package/out/main/common/power-logs-limits.js +227 -0
  5. package/out/main/common/power-logs-limits.js.map +1 -0
  6. package/out/main/common/power-logs.js +32 -0
  7. package/out/main/common/power-logs.js.map +1 -0
  8. package/out/main/common/resource-search.js +61 -0
  9. package/out/main/common/resource-search.js.map +1 -0
  10. package/out/main/common/store/logs-preferences-store.js +45 -0
  11. package/out/main/common/store/logs-preferences-store.js.map +1 -0
  12. package/out/main/common/store/monitoring-preferences-store.js +5 -0
  13. package/out/main/common/store/monitoring-preferences-store.js.map +1 -1
  14. package/out/main/dashboard-icon-cache.js +1 -1
  15. package/out/main/index.js +6 -0
  16. package/out/main/index.js.map +1 -1
  17. package/out/main/power-logs-budget.js +126 -0
  18. package/out/main/power-logs-budget.js.map +1 -0
  19. package/out/main/power-logs-child.js +86 -0
  20. package/out/main/power-logs-child.js.map +1 -0
  21. package/out/main/power-logs-database.js +179 -0
  22. package/out/main/power-logs-database.js.map +1 -0
  23. package/out/main/power-logs-ipc.js +275 -0
  24. package/out/main/power-logs-ipc.js.map +1 -0
  25. package/out/main/power-logs-query-child.js +23 -0
  26. package/out/main/power-logs-query-child.js.map +1 -0
  27. package/out/main/power-logs-query.js +127 -0
  28. package/out/main/power-logs-query.js.map +1 -0
  29. package/out/main/power-logs-worker.js +215 -0
  30. package/out/main/power-logs-worker.js.map +1 -0
  31. package/out/main/resource-search-database.js +209 -0
  32. package/out/main/resource-search-database.js.map +1 -0
  33. package/out/main/resource-search-worker.js +56 -0
  34. package/out/main/resource-search-worker.js.map +1 -0
  35. package/out/main/resource-search.js +155 -0
  36. package/out/main/resource-search.js.map +1 -0
  37. package/out/renderer/ai-logs-sql.js +27 -0
  38. package/out/renderer/ai-logs-sql.js.map +1 -0
  39. package/out/renderer/ai-message.js +48 -27
  40. package/out/renderer/ai-message.js.map +1 -1
  41. package/out/renderer/ai-terminal-context.js +50 -40
  42. package/out/renderer/ai-terminal-context.js.map +1 -1
  43. package/out/renderer/ai.js +15 -2
  44. package/out/renderer/ai.js.map +1 -1
  45. package/out/renderer/appearance.js +2 -1
  46. package/out/renderer/appearance.js.map +1 -1
  47. package/out/renderer/gateway-relationships-client.js +107 -0
  48. package/out/renderer/gateway-relationships-client.js.map +1 -0
  49. package/out/renderer/index.js +67 -29
  50. package/out/renderer/index.js.map +1 -1
  51. package/out/renderer/power-log-json.js +29 -0
  52. package/out/renderer/power-log-json.js.map +1 -0
  53. package/out/renderer/power-logs-client.js +35 -0
  54. package/out/renderer/power-logs-client.js.map +1 -0
  55. package/out/renderer/power-logs-collector.js +316 -0
  56. package/out/renderer/power-logs-collector.js.map +1 -0
  57. package/out/renderer/power-logs-entrypoints.js +201 -0
  58. package/out/renderer/power-logs-entrypoints.js.map +1 -0
  59. package/out/renderer/power-logs-sql-row.js +52 -0
  60. package/out/renderer/power-logs-sql-row.js.map +1 -0
  61. package/out/renderer/power-logs-transport.js +122 -0
  62. package/out/renderer/power-logs-transport.js.map +1 -0
  63. package/out/renderer/power-logs.css.js +116 -0
  64. package/out/renderer/power-logs.css.js.map +1 -0
  65. package/out/renderer/power-logs.js +1352 -0
  66. package/out/renderer/power-logs.js.map +1 -0
  67. package/out/renderer/preferences/logs-preferences.js +93 -0
  68. package/out/renderer/preferences/logs-preferences.js.map +1 -0
  69. package/out/renderer/preferences/monitoring-preferences.js +10 -0
  70. package/out/renderer/preferences/monitoring-preferences.js.map +1 -1
  71. package/out/renderer/preferences/preferences.js +24 -10
  72. package/out/renderer/preferences/preferences.js.map +1 -1
  73. package/out/renderer/resource-relationships.css.js +1 -1
  74. package/out/renderer/resource-relationships.css.js.map +1 -1
  75. package/out/renderer/resource-relationships.js +46 -8
  76. package/out/renderer/resource-relationships.js.map +1 -1
  77. package/out/renderer/resource-search-client.js +116 -0
  78. package/out/renderer/resource-search-client.js.map +1 -0
  79. package/out/renderer/resource-search-discovery.js +79 -0
  80. package/out/renderer/resource-search-discovery.js.map +1 -0
  81. package/out/renderer/resource-search-model.js +2 -1
  82. package/out/renderer/resource-search-model.js.map +1 -1
  83. package/out/renderer/resource-search-routing.js +78 -0
  84. package/out/renderer/resource-search-routing.js.map +1 -0
  85. package/out/renderer/resource-search-sync.js +199 -0
  86. package/out/renderer/resource-search-sync.js.map +1 -0
  87. package/out/renderer/resource-search.js +279 -54
  88. package/out/renderer/resource-search.js.map +1 -1
  89. package/out/renderer/src/common/gateway-relationships.js +122 -0
  90. package/out/renderer/src/common/gateway-relationships.js.map +1 -0
  91. package/out/renderer/src/common/power-log-parser.js +114 -0
  92. package/out/renderer/src/common/power-log-parser.js.map +1 -0
  93. package/out/renderer/src/common/power-logs-ai.js +56 -0
  94. package/out/renderer/src/common/power-logs-ai.js.map +1 -0
  95. package/out/renderer/src/common/power-logs-limits.js +227 -0
  96. package/out/renderer/src/common/power-logs-limits.js.map +1 -0
  97. package/out/renderer/src/common/power-logs.js +32 -0
  98. package/out/renderer/src/common/power-logs.js.map +1 -0
  99. package/out/renderer/src/common/resource-relationships.js +8 -0
  100. package/out/renderer/src/common/resource-relationships.js.map +1 -1
  101. package/out/renderer/src/common/resource-search.js +6 -0
  102. package/out/renderer/src/common/resource-search.js.map +1 -0
  103. package/out/renderer/src/common/store/logs-preferences-store.js +45 -0
  104. package/out/renderer/src/common/store/logs-preferences-store.js.map +1 -0
  105. package/out/renderer/src/common/store/monitoring-preferences-store.js +5 -0
  106. package/out/renderer/src/common/store/monitoring-preferences-store.js.map +1 -1
  107. package/package.json +8 -2
@@ -0,0 +1,155 @@
1
+ const require_extensions = require("./_virtual/_global-external_@freelensapp/extensions.js");
2
+ const require__global_external_mobx = require("./_virtual/_global-external_mobx.js");
3
+ const require_monitoring_preferences_store = require("./common/store/monitoring-preferences-store.js");
4
+ const require_resource_search = require("./common/resource-search.js");
5
+ const require_resource_search_worker = require("./resource-search-worker.js");
6
+ let node_crypto = require("node:crypto");
7
+ let electron = require("electron");
8
+ let node_path = require("node:path");
9
+ let node_fs = require("node:fs");
10
+ //#region src/main/resource-search.ts
11
+ function trustedSearchSender(event, clusterId) {
12
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
13
+ const frame = event.senderFrame;
14
+ if (!window || window.isDestroyed() || !frame) return false;
15
+ try {
16
+ const url = new URL(frame.url);
17
+ const root = new URL(event.sender.getURL());
18
+ if (url.protocol !== "https:" || root.protocol !== "https:" || url.username || url.password || root.username || root.password) return false;
19
+ if (url.hostname !== `${clusterId}.renderer.freelens.app` || root.port !== url.port) return false;
20
+ if (frame === event.sender.mainFrame) return root.origin === url.origin;
21
+ return root.hostname === "renderer.freelens.app" && frame.parent === event.sender.mainFrame && frame.top === event.sender.mainFrame;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function validateSearchRequest(input) {
27
+ if (!require_resource_search.validSearchPartition(input)) throw new Error("Invalid search partition");
28
+ const request = input;
29
+ const partition = {
30
+ clusterId: request.clusterId,
31
+ contextId: request.contextId
32
+ };
33
+ if (request.operation === "reconcile") {
34
+ if (!Array.isArray(request.resources) || request.resources.length > 1e4 || request.resources.some((resource) => typeof resource !== "string" || !/^[a-zA-Z0-9_.-]{0,253}\/[a-zA-Z0-9_.-]{1,253}$/.test(resource))) throw new Error("Invalid search resource catalog");
35
+ return {
36
+ ...partition,
37
+ operation: "reconcile",
38
+ resources: [...new Set(request.resources)]
39
+ };
40
+ }
41
+ if ([
42
+ "begin",
43
+ "append",
44
+ "complete",
45
+ "invalidate"
46
+ ].includes(request.operation)) {
47
+ const scoped = request;
48
+ if (typeof scoped.scope !== "string" || !scoped.scope.length || scoped.scope.length > 512 || /[\x00-\x1f]/.test(scoped.scope)) throw new Error("Invalid search scope");
49
+ if (scoped.operation !== "invalidate" && (typeof scoped.scanId !== "string" || !/^[a-zA-Z0-9_.:-]{1,128}$/.test(scoped.scanId))) throw new Error("Invalid search scan");
50
+ if (scoped.operation === "invalidate") return {
51
+ ...partition,
52
+ operation: "invalidate",
53
+ scope: scoped.scope
54
+ };
55
+ const scan = {
56
+ ...partition,
57
+ scope: scoped.scope,
58
+ scanId: scoped.scanId
59
+ };
60
+ return scoped.operation === "append" ? {
61
+ ...scan,
62
+ operation: "append",
63
+ documents: require_resource_search.projectSearchDocuments(scoped.documents)
64
+ } : {
65
+ ...scan,
66
+ operation: scoped.operation
67
+ };
68
+ }
69
+ if (request.operation === "query") {
70
+ if (typeof request.query !== "string" || request.query.length > 300 || request.limit !== void 0 && (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 1e3) || request.kind !== void 0 && !/^[a-zA-Z0-9_.:-]{1,253}$/.test(request.kind) || request.namespace !== void 0 && !/^[a-zA-Z0-9_.:-]{0,253}$/.test(request.namespace)) throw new Error("Invalid search query");
71
+ return {
72
+ ...partition,
73
+ operation: "query",
74
+ query: request.query,
75
+ limit: request.limit,
76
+ kind: request.kind,
77
+ namespace: request.namespace
78
+ };
79
+ }
80
+ if (request.operation !== "status" && request.operation !== "clear" && request.operation !== "permit") throw new Error("Invalid search operation");
81
+ return {
82
+ ...partition,
83
+ operation: request.operation
84
+ };
85
+ }
86
+ function startResourceSearchIndex() {
87
+ let active = true;
88
+ let worker;
89
+ const preferences = require_monitoring_preferences_store.MonitoringPreferencesStore.getInstanceOrCreate();
90
+ const stopPreference = require__global_external_mobx.reaction(() => preferences.resourceSearchEnabled, (enabled) => {
91
+ if (!enabled) {
92
+ worker?.close();
93
+ worker = void 0;
94
+ }
95
+ });
96
+ const nextRequest = /* @__PURE__ */ new Map();
97
+ const waits = /* @__PURE__ */ new Map();
98
+ const contextFingerprint = (request) => {
99
+ const cluster = require_extensions.Main.Catalog.getClusterById(request.clusterId);
100
+ if (!cluster) throw new Error("Unknown search cluster");
101
+ return (0, node_crypto.createHash)("sha256").update(request.contextId).update("\0").update(cluster.contextName).update("\0").update(cluster.kubeConfigPath).update("\0").update((0, node_fs.readFileSync)(cluster.kubeConfigPath)).digest("hex");
102
+ };
103
+ electron.ipcMain.handle(require_resource_search.RESOURCE_SEARCH_CHANNEL, async (event, input) => {
104
+ const request = validateSearchRequest(input);
105
+ if (!active || !trustedSearchSender(event, request.clusterId)) throw new Error("Search index unavailable");
106
+ const contextId = contextFingerprint(request);
107
+ if (request.operation === "permit") {
108
+ const start = Math.max(Date.now(), nextRequest.get(request.clusterId) ?? 0);
109
+ nextRequest.set(request.clusterId, start + 500);
110
+ if (start > Date.now()) await new Promise((resolve) => {
111
+ const timer = setTimeout(() => {
112
+ waits.delete(timer);
113
+ resolve();
114
+ }, start - Date.now());
115
+ waits.set(timer, resolve);
116
+ });
117
+ if (!active || !trustedSearchSender(event, request.clusterId) || contextFingerprint(request) !== contextId) throw new Error("Search context changed");
118
+ return true;
119
+ }
120
+ if (!preferences.resourceSearchEnabled) throw new Error("Resource search is disabled");
121
+ if (!worker) {
122
+ const directory = (0, node_path.join)(electron.app.getPath("userData"), "freelens-power-search");
123
+ (0, node_fs.mkdirSync)(directory, {
124
+ recursive: true,
125
+ mode: 448
126
+ });
127
+ worker = new require_resource_search_worker.ResourceSearchWorker((0, node_path.join)(directory, "resources.sqlite"));
128
+ }
129
+ const result = await worker.call({
130
+ ...request,
131
+ contextId
132
+ });
133
+ if (!active || !preferences.resourceSearchEnabled || !trustedSearchSender(event, request.clusterId) || contextFingerprint(request) !== contextId) throw new Error("Search context changed");
134
+ return result;
135
+ });
136
+ return () => {
137
+ active = false;
138
+ stopPreference();
139
+ electron.ipcMain.removeHandler(require_resource_search.RESOURCE_SEARCH_CHANNEL);
140
+ for (const [timer, resolve] of waits) {
141
+ clearTimeout(timer);
142
+ resolve();
143
+ }
144
+ waits.clear();
145
+ nextRequest.clear();
146
+ worker?.close();
147
+ worker = void 0;
148
+ };
149
+ }
150
+ //#endregion
151
+ exports.startResourceSearchIndex = startResourceSearchIndex;
152
+ exports.trustedSearchSender = trustedSearchSender;
153
+ exports.validateSearchRequest = validateSearchRequest;
154
+
155
+ //# sourceMappingURL=resource-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search.js","names":[],"sources":["../../src/main/resource-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Main } from \"@freelensapp/extensions\";\nimport { app, BrowserWindow, type IpcMainInvokeEvent, ipcMain } from \"electron\";\nimport { reaction } from \"mobx\";\nimport {\n projectSearchDocuments,\n RESOURCE_SEARCH_CHANNEL,\n type SearchIndexRequest,\n validSearchPartition,\n} from \"../common/resource-search\";\nimport { MonitoringPreferencesStore } from \"../common/store/monitoring-preferences-store\";\nimport { ResourceSearchWorker } from \"./resource-search-worker\";\n\nexport function trustedSearchSender(event: IpcMainInvokeEvent, clusterId: string): boolean {\n const window = BrowserWindow.fromWebContents(event.sender);\n const frame = event.senderFrame;\n if (!window || window.isDestroyed() || !frame) return false;\n try {\n const url = new URL(frame.url);\n const root = new URL(event.sender.getURL());\n if (\n url.protocol !== \"https:\" ||\n root.protocol !== \"https:\" ||\n url.username ||\n url.password ||\n root.username ||\n root.password\n )\n return false;\n if (url.hostname !== `${clusterId}.renderer.freelens.app` || root.port !== url.port) return false;\n if (frame === event.sender.mainFrame) return root.origin === url.origin;\n return (\n root.hostname === \"renderer.freelens.app\" &&\n frame.parent === event.sender.mainFrame &&\n frame.top === event.sender.mainFrame\n );\n } catch {\n return false;\n }\n}\n\nexport function validateSearchRequest(input: unknown): SearchIndexRequest {\n if (!validSearchPartition(input)) throw new Error(\"Invalid search partition\");\n const request = input as SearchIndexRequest;\n const partition = { clusterId: request.clusterId, contextId: request.contextId };\n if (request.operation === \"reconcile\") {\n if (\n !Array.isArray(request.resources) ||\n request.resources.length > 10000 ||\n request.resources.some(\n (resource) => typeof resource !== \"string\" || !/^[a-zA-Z0-9_.-]{0,253}\\/[a-zA-Z0-9_.-]{1,253}$/.test(resource),\n )\n )\n throw new Error(\"Invalid search resource catalog\");\n return { ...partition, operation: \"reconcile\", resources: [...new Set(request.resources)] };\n }\n if ([\"begin\", \"append\", \"complete\", \"invalidate\"].includes(request.operation)) {\n const scoped = request as Extract<SearchIndexRequest, { scope: string }>;\n if (\n typeof scoped.scope !== \"string\" ||\n !scoped.scope.length ||\n scoped.scope.length > 512 ||\n /[\\x00-\\x1f]/.test(scoped.scope)\n )\n throw new Error(\"Invalid search scope\");\n if (\n scoped.operation !== \"invalidate\" &&\n (typeof scoped.scanId !== \"string\" || !/^[a-zA-Z0-9_.:-]{1,128}$/.test(scoped.scanId))\n )\n throw new Error(\"Invalid search scan\");\n if (scoped.operation === \"invalidate\") return { ...partition, operation: \"invalidate\", scope: scoped.scope };\n const scan = { ...partition, scope: scoped.scope, scanId: scoped.scanId };\n return scoped.operation === \"append\"\n ? { ...scan, operation: \"append\", documents: projectSearchDocuments(scoped.documents) }\n : { ...scan, operation: scoped.operation };\n }\n if (request.operation === \"query\") {\n if (\n typeof request.query !== \"string\" ||\n request.query.length > 300 ||\n (request.limit !== undefined &&\n (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 1000)) ||\n (request.kind !== undefined && !/^[a-zA-Z0-9_.:-]{1,253}$/.test(request.kind)) ||\n (request.namespace !== undefined && !/^[a-zA-Z0-9_.:-]{0,253}$/.test(request.namespace))\n )\n throw new Error(\"Invalid search query\");\n return {\n ...partition,\n operation: \"query\",\n query: request.query,\n limit: request.limit,\n kind: request.kind,\n namespace: request.namespace,\n };\n }\n if (request.operation !== \"status\" && request.operation !== \"clear\" && request.operation !== \"permit\")\n throw new Error(\"Invalid search operation\");\n return { ...partition, operation: request.operation };\n}\n\nexport function startResourceSearchIndex(): () => void {\n let active = true;\n let worker: ResourceSearchWorker | undefined;\n const preferences = MonitoringPreferencesStore.getInstanceOrCreate<MonitoringPreferencesStore>();\n const stopPreference = reaction(\n () => preferences.resourceSearchEnabled,\n (enabled) => {\n if (!enabled) {\n worker?.close();\n worker = undefined;\n }\n },\n );\n const nextRequest = new Map<string, number>();\n const waits = new Map<ReturnType<typeof setTimeout>, () => void>();\n const contextFingerprint = (request: SearchIndexRequest) => {\n const cluster = Main.Catalog.getClusterById(request.clusterId);\n if (!cluster) throw new Error(\"Unknown search cluster\");\n // only a digest of the local configuration reaches the database partition key\n return createHash(\"sha256\")\n .update(request.contextId)\n .update(\"\\0\")\n .update(cluster.contextName)\n .update(\"\\0\")\n .update(cluster.kubeConfigPath)\n .update(\"\\0\")\n .update(readFileSync(cluster.kubeConfigPath))\n .digest(\"hex\");\n };\n ipcMain.handle(RESOURCE_SEARCH_CHANNEL, async (event, input: unknown) => {\n const request = validateSearchRequest(input);\n if (!active || !trustedSearchSender(event, request.clusterId)) throw new Error(\"Search index unavailable\");\n const contextId = contextFingerprint(request);\n if (request.operation === \"permit\") {\n const start = Math.max(Date.now(), nextRequest.get(request.clusterId) ?? 0);\n nextRequest.set(request.clusterId, start + 500);\n if (start > Date.now())\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n waits.delete(timer);\n resolve();\n }, start - Date.now());\n waits.set(timer, resolve);\n });\n if (!active || !trustedSearchSender(event, request.clusterId) || contextFingerprint(request) !== contextId)\n throw new Error(\"Search context changed\");\n return true;\n }\n if (!preferences.resourceSearchEnabled) throw new Error(\"Resource search is disabled\");\n if (!worker) {\n const directory = join(app.getPath(\"userData\"), \"freelens-power-search\");\n mkdirSync(directory, { recursive: true, mode: 0o700 });\n worker = new ResourceSearchWorker(join(directory, \"resources.sqlite\"));\n }\n const result = await worker.call({ ...request, contextId });\n if (\n !active ||\n !preferences.resourceSearchEnabled ||\n !trustedSearchSender(event, request.clusterId) ||\n contextFingerprint(request) !== contextId\n )\n throw new Error(\"Search context changed\");\n return result;\n });\n return () => {\n active = false;\n stopPreference();\n ipcMain.removeHandler(RESOURCE_SEARCH_CHANNEL);\n for (const [timer, resolve] of waits) {\n clearTimeout(timer);\n resolve();\n }\n waits.clear();\n nextRequest.clear();\n worker?.close();\n worker = undefined;\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ const require__global_external_react = require("./_virtual/_global-external_react.js");
2
+ //#region src/renderer/ai-logs-sql.ts
3
+ var POWER_LOGS_SQL_DRAFT_PROMPT = `On the Power Logs workspace, help draft SQLite SELECT queries for captured logs.
4
+ The table is logs(timestamp INTEGER, pod TEXT, podUid TEXT, container TEXT, level TEXT, message TEXT, fieldsJson TEXT, previous INTEGER).
5
+ Timestamp is Unix epoch milliseconds and can be NULL. previous is 0 or 1. fieldsJson contains JSON text or NULL, usable with SQLite json_extract.
6
+ Return proposed queries in fenced sql blocks. Draft queries only: never execute them, call a query tool, or claim they ran. The user can insert a draft into the SQL editor, review it, and explicitly run it.`;
7
+ function isPowerLogsWorkspace() {
8
+ return !!document.querySelector("[data-testid=\"power-logs-page\"]") || /\/power-logs\/?$/.test(window.location.pathname);
9
+ }
10
+ function usePowerLogsWorkspace() {
11
+ const [active, setActive] = require__global_external_react.useState(isPowerLogsWorkspace);
12
+ require__global_external_react.useEffect(() => {
13
+ const update = (event) => {
14
+ const value = event.detail?.active;
15
+ if (typeof value === "boolean") setActive(value);
16
+ };
17
+ window.addEventListener("power-logs-workspace-change", update);
18
+ return () => window.removeEventListener("power-logs-workspace-change", update);
19
+ }, []);
20
+ return active;
21
+ }
22
+ //#endregion
23
+ exports.POWER_LOGS_SQL_DRAFT_PROMPT = POWER_LOGS_SQL_DRAFT_PROMPT;
24
+ exports.isPowerLogsWorkspace = isPowerLogsWorkspace;
25
+ exports.usePowerLogsWorkspace = usePowerLogsWorkspace;
26
+
27
+ //# sourceMappingURL=ai-logs-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-logs-sql.js","names":[],"sources":["../../src/renderer/ai-logs-sql.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\n\nexport const POWER_LOGS_SQL_DRAFT_PROMPT = `On the Power Logs workspace, help draft SQLite SELECT queries for captured logs.\nThe table is logs(timestamp INTEGER, pod TEXT, podUid TEXT, container TEXT, level TEXT, message TEXT, fieldsJson TEXT, previous INTEGER).\nTimestamp is Unix epoch milliseconds and can be NULL. previous is 0 or 1. fieldsJson contains JSON text or NULL, usable with SQLite json_extract.\nReturn proposed queries in fenced sql blocks. Draft queries only: never execute them, call a query tool, or claim they ran. The user can insert a draft into the SQL editor, review it, and explicitly run it.`;\n\nexport function isPowerLogsWorkspace(): boolean {\n return (\n !!document.querySelector('[data-testid=\"power-logs-page\"]') || /\\/power-logs\\/?$/.test(window.location.pathname)\n );\n}\n\nexport function usePowerLogsWorkspace(): boolean {\n const [active, setActive] = useState(isPowerLogsWorkspace);\n useEffect(() => {\n const update = (event: Event) => {\n const value = (event as CustomEvent<{ active?: unknown }>).detail?.active;\n if (typeof value === \"boolean\") setActive(value);\n };\n window.addEventListener(\"power-logs-workspace-change\", update);\n return () => window.removeEventListener(\"power-logs-workspace-change\", update);\n }, []);\n return active;\n}\n"],"mappings":";;AAEA,IAAa,8BAA8B;;;;AAK3C,SAAgB,uBAAgC;CAC9C,OACE,CAAC,CAAC,SAAS,cAAc,mCAAiC,KAAK,mBAAmB,KAAK,OAAO,SAAS,QAAQ;AAEnH;AAEA,SAAgB,wBAAiC;CAC/C,MAAM,CAAC,QAAQ,aAAa,+BAAA,SAAS,oBAAoB;CACzD,+BAAA,gBAAgB;EACd,MAAM,UAAU,UAAiB;GAC/B,MAAM,QAAS,MAA4C,QAAQ;GACnE,IAAI,OAAO,UAAU,WAAW,UAAU,KAAK;EACjD;EACA,OAAO,iBAAiB,+BAA+B,MAAM;EAC7D,aAAa,OAAO,oBAAoB,+BAA+B,MAAM;CAC/E,GAAG,CAAC,CAAC;CACL,OAAO;AACT"}
@@ -3,6 +3,7 @@ const require__global_external_mobx = require("./_virtual/_global-external_mobx.
3
3
  const require__global_external_react = require("./_virtual/_global-external_react.js");
4
4
  const require_jsx_runtime = require("./_virtual/_global-external_react/jsx-runtime.js");
5
5
  const require_ai_icon = require("./ai-icon.js");
6
+ const require_ai_logs_sql = require("./ai-logs-sql.js");
6
7
  const require_markdown_it = require("./node_modules/.pnpm/markdown-it@15.0.2/node_modules/markdown-it/dist/markdown-it.js");
7
8
  let electron = require("electron");
8
9
  //#region src/renderer/ai-message.tsx
@@ -185,6 +186,8 @@ function CodeBlock({ text, language }) {
185
186
  "terminal"
186
187
  ].includes(language.toLowerCase());
187
188
  const command = terminalInsertText(text);
189
+ const logsWorkspace = require_ai_logs_sql.usePowerLogsWorkspace();
190
+ const sqlDraft = ["sql", "sqlite"].includes(language.toLowerCase()) && logsWorkspace;
188
191
  const insert = async () => {
189
192
  const activeTerminal = visibleTerminalId();
190
193
  if (!shell || !command || !activeTerminal || inserting) return;
@@ -206,33 +209,51 @@ function CodeBlock({ text, language }) {
206
209
  children: [
207
210
  /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
208
211
  className: "power-ai-code-toolbar",
209
- children: [/* @__PURE__ */ require_jsx_runtime.jsx("span", { children: language || "code" }), /* @__PURE__ */ require_jsx_runtime.jsxs("div", { children: [/* @__PURE__ */ require_jsx_runtime.jsxs("button", {
210
- type: "button",
211
- "aria-label": "Copy code",
212
- title: "Copy code to clipboard",
213
- onClick: () => {
214
- try {
215
- electron.clipboard.writeText(text);
216
- setFeedback("Copied");
217
- } catch {
218
- setFeedback("Unable to copy code.");
219
- }
220
- },
221
- children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
222
- material: "content_copy",
223
- size: 14
224
- }), " Copy"]
225
- }), shell && terminalId && /* @__PURE__ */ require_jsx_runtime.jsxs("button", {
226
- type: "button",
227
- "aria-label": "Insert into terminal",
228
- disabled: !command || !terminalId || inserting,
229
- title: !command ? "Only a single line without control characters can be inserted" : !terminalId ? "Focus an open terminal tab to insert" : "Insert without running",
230
- onClick: () => void insert(),
231
- children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
232
- material: "terminal",
233
- size: 14
234
- }), " Insert"]
235
- })] })]
212
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx("span", { children: language || "code" }), /* @__PURE__ */ require_jsx_runtime.jsxs("div", { children: [
213
+ /* @__PURE__ */ require_jsx_runtime.jsxs("button", {
214
+ type: "button",
215
+ "aria-label": "Copy code",
216
+ title: "Copy code to clipboard",
217
+ onClick: () => {
218
+ try {
219
+ electron.clipboard.writeText(text);
220
+ setFeedback("Copied");
221
+ } catch {
222
+ setFeedback("Unable to copy code.");
223
+ }
224
+ },
225
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
226
+ material: "content_copy",
227
+ size: 14
228
+ }), " Copy"]
229
+ }),
230
+ sqlDraft && /* @__PURE__ */ require_jsx_runtime.jsxs("button", {
231
+ type: "button",
232
+ "aria-label": "Insert into SQL",
233
+ title: "Insert into the SQL editor without running",
234
+ disabled: !text.trim() || text.length > 65536,
235
+ onClick: () => {
236
+ if (!require_ai_logs_sql.isPowerLogsWorkspace() || !text.trim() || text.length > 65536) return;
237
+ window.dispatchEvent(new CustomEvent("power-logs-sql-draft", { detail: { sql: text } }));
238
+ window.dispatchEvent(new Event("power-ai-close"));
239
+ },
240
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
241
+ material: "post_add",
242
+ size: 14
243
+ }), " Insert into SQL"]
244
+ }),
245
+ shell && terminalId && /* @__PURE__ */ require_jsx_runtime.jsxs("button", {
246
+ type: "button",
247
+ "aria-label": "Insert into terminal",
248
+ disabled: !command || !terminalId || inserting,
249
+ title: !command ? "Only a single line without control characters can be inserted" : !terminalId ? "Focus an open terminal tab to insert" : "Insert without running",
250
+ onClick: () => void insert(),
251
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
252
+ material: "terminal",
253
+ size: 14
254
+ }), " Insert"]
255
+ })
256
+ ] })]
236
257
  }),
237
258
  /* @__PURE__ */ require_jsx_runtime.jsx("pre", { children: /* @__PURE__ */ require_jsx_runtime.jsx("code", { children: text }) }),
238
259
  shell && !command && /* @__PURE__ */ require_jsx_runtime.jsx("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"ai-message.js","names":[],"sources":["../../src/renderer/ai-message.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { clipboard, shell } from \"electron\";\nimport MarkdownIt, { type Token } from \"markdown-it\";\nimport { autorun } from \"mobx\";\nimport { createElement, Fragment, type ReactNode, useEffect, useMemo, useState } from \"react\";\nimport { AiIcon } from \"./ai-icon\";\n\nconst markdown = new MarkdownIt({ html: false, linkify: true, breaks: true });\nconst markdownTags = new Set([\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"ul\",\n \"ol\",\n \"li\",\n \"blockquote\",\n \"table\",\n \"thead\",\n \"tbody\",\n \"tr\",\n \"th\",\n \"td\",\n \"strong\",\n \"em\",\n \"s\",\n \"a\",\n]);\n\nfunction externalLink(value: string | number | null): string | undefined {\n if (typeof value !== \"string\" || !value) return;\n try {\n const url = new URL(value);\n if ([\"https:\", \"http:\", \"mailto:\"].includes(url.protocol) && !url.username && !url.password) return url.href;\n } catch {}\n return;\n}\n\nfunction renderTokens(tokens: Token[]): ReactNode[] {\n type Container = { token?: Token; key: number; children: ReactNode[] };\n const stack: Container[] = [{ key: 0, children: [] }];\n const append = (node: ReactNode) => stack[stack.length - 1].children.push(node);\n tokens.forEach((token, index) => {\n if (token.nesting === 1) {\n stack.push({ token, key: index, children: [] });\n return;\n }\n if (token.nesting === -1) {\n if (stack.length === 1) return;\n const container = stack.pop()!;\n const opening = container.token!;\n const props: Record<string, unknown> = { key: container.key };\n let tag = markdownTags.has(opening.tag) && !opening.hidden ? opening.tag : Fragment;\n if (tag === \"a\") {\n const href = externalLink(opening.attrGet(\"href\"));\n if (!href) tag = Fragment;\n else {\n props.href = href;\n props.title = opening.attrGet(\"title\") ?? undefined;\n props.rel = \"noopener noreferrer\";\n props.target = \"_blank\";\n const open = (event: React.MouseEvent<HTMLAnchorElement>) => {\n event.preventDefault();\n if (event.type === \"auxclick\" && event.button !== 1) return;\n void shell.openExternal(href).catch(() => {});\n };\n props.onClick = open;\n props.onAuxClick = open;\n }\n }\n if (tag === \"ol\") {\n const start = Number(opening.attrGet(\"start\"));\n if (Number.isSafeInteger(start) && start > 0) props.start = start;\n }\n if (tag === \"th\" || tag === \"td\") {\n const alignment = String(opening.attrGet(\"style\") ?? \"\").match(/^text-align:(left|right|center)$/)?.[1];\n if (alignment) props.style = { textAlign: alignment };\n }\n append(createElement(tag, props, ...container.children));\n return;\n }\n switch (token.type) {\n case \"inline\":\n append(<Fragment key={index}>{renderTokens(token.children ?? [])}</Fragment>);\n break;\n case \"code_inline\":\n append(<code key={index}>{token.content}</code>);\n break;\n case \"fence\":\n case \"code_block\":\n append(<CodeBlock key={index} language={token.info.trim().split(/\\s+/)[0]} text={token.content} />);\n break;\n case \"hardbreak\":\n case \"softbreak\":\n append(<br key={index} />);\n break;\n case \"hr\":\n append(<hr key={index} />);\n break;\n default:\n append(<Fragment key={index}>{token.content}</Fragment>);\n }\n });\n return stack[0].children;\n}\n\nexport function terminalInsertText(text: string): string | undefined {\n const command = text.replace(/^[\\r\\n]+|[\\r\\n]+$/g, \"\");\n // surrounding line endings are formatting, embedded control characters must never reach the terminal\n if (!command.trim() || /[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/u.test(command)) return undefined;\n return command;\n}\n\nfunction visibleTerminalId(): string | undefined {\n const prefix = \"dock-tab-content-for-\";\n for (const element of document.querySelectorAll<HTMLElement>(`[data-testid^=\"${prefix}\"]`)) {\n let visible = true;\n for (let node: HTMLElement | null = element; node; node = node.parentElement) {\n const style = getComputedStyle(node);\n if (\n node.hidden ||\n node.getAttribute(\"aria-hidden\") === \"true\" ||\n style.display === \"none\" ||\n style.visibility === \"hidden\" ||\n style.visibility === \"collapse\"\n ) {\n visible = false;\n break;\n }\n }\n if (!visible) continue;\n const id = element.dataset.testid?.slice(prefix.length);\n if (!id || !Renderer.Component.terminalStore.isConnected(id)) continue;\n const api = Renderer.Component.terminalStore.getTerminalApi(id);\n if (api?.isReady && api.isConnected()) return id;\n }\n return;\n}\n\nfunction useActiveTerminalId(): string | undefined {\n const [terminalId, setTerminalId] = useState<string>();\n useEffect(() => {\n let stop: (() => void) | undefined;\n const update = () => {\n stop?.();\n stop = autorun(() => setTerminalId(visibleTerminalId()));\n };\n update();\n const observer = new MutationObserver(update);\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"hidden\", \"aria-hidden\", \"data-testid\"],\n });\n return () => {\n observer.disconnect();\n stop?.();\n };\n }, []);\n return terminalId;\n}\n\nfunction CodeBlock({ text, language }: { text: string; language: string }) {\n const [feedback, setFeedback] = useState(\"\");\n const terminalId = useActiveTerminalId();\n const [inserting, setInserting] = useState(false);\n const shell = [\"\", \"bash\", \"sh\", \"shell\", \"zsh\", \"console\", \"terminal\"].includes(language.toLowerCase());\n const command = terminalInsertText(text);\n const insert = async () => {\n const activeTerminal = visibleTerminalId();\n if (!shell || !command || !activeTerminal || inserting) return;\n setInserting(true);\n try {\n await Renderer.Component.terminalStore.sendCommand(command, { enter: false, tabId: activeTerminal });\n setFeedback(\"Inserted into terminal. Review the command before running it.\");\n } catch {\n setFeedback(\"Unable to insert into terminal.\");\n } finally {\n setInserting(false);\n }\n };\n return (\n <div className=\"power-ai-code\">\n <div className=\"power-ai-code-toolbar\">\n <span>{language || \"code\"}</span>\n <div>\n <button\n type=\"button\"\n aria-label=\"Copy code\"\n title=\"Copy code to clipboard\"\n onClick={() => {\n try {\n clipboard.writeText(text);\n setFeedback(\"Copied\");\n } catch {\n setFeedback(\"Unable to copy code.\");\n }\n }}\n >\n <AiIcon material=\"content_copy\" size={14} /> Copy\n </button>\n {shell && terminalId && (\n <button\n type=\"button\"\n aria-label=\"Insert into terminal\"\n disabled={!command || !terminalId || inserting}\n title={\n !command\n ? \"Only a single line without control characters can be inserted\"\n : !terminalId\n ? \"Focus an open terminal tab to insert\"\n : \"Insert without running\"\n }\n onClick={() => void insert()}\n >\n <AiIcon material=\"terminal\" size={14} /> Insert\n </button>\n )}\n </div>\n </div>\n <pre>\n <code>{text}</code>\n </pre>\n {shell && !command && (\n <div className=\"power-ai-code-hint\">Terminal insertion requires a single line without control characters.</div>\n )}\n <div role=\"status\" className=\"power-ai-code-feedback\">\n {feedback}\n </div>\n </div>\n );\n}\n\nexport function AiMessageContent({ text }: { text: string }) {\n const content = useMemo(() => renderTokens(markdown.parse(text, {})), [text]);\n return <div className=\"power-ai-markdown\">{content}</div>;\n}\n"],"mappings":";;;;;;;;AAOA,IAAM,WAAW,IAAI,oBAAA,QAAW;CAAE,MAAM;CAAO,SAAS;CAAM,QAAQ;AAAK,CAAC;AAC5E,IAAM,+BAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,aAAa,OAAmD;CACvE,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO;CACzC,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,KAAK;EACzB,IAAI;GAAC;GAAU;GAAS;EAAS,CAAC,CAAC,SAAS,IAAI,QAAQ,KAAK,CAAC,IAAI,YAAY,CAAC,IAAI,UAAU,OAAO,IAAI;CAC1G,QAAQ,CAAC;AAEX;AAEA,SAAS,aAAa,QAA8B;CAElD,MAAM,QAAqB,CAAC;EAAE,KAAK;EAAG,UAAU,CAAC;CAAE,CAAC;CACpD,MAAM,UAAU,SAAoB,MAAM,MAAM,SAAS,EAAE,CAAC,SAAS,KAAK,IAAI;CAC9E,OAAO,SAAS,OAAO,UAAU;EAC/B,IAAI,MAAM,YAAY,GAAG;GACvB,MAAM,KAAK;IAAE;IAAO,KAAK;IAAO,UAAU,CAAC;GAAE,CAAC;GAC9C;EACF;EACA,IAAI,MAAM,YAAY,IAAI;GACxB,IAAI,MAAM,WAAW,GAAG;GACxB,MAAM,YAAY,MAAM,IAAI;GAC5B,MAAM,UAAU,UAAU;GAC1B,MAAM,QAAiC,EAAE,KAAK,UAAU,IAAI;GAC5D,IAAI,MAAM,aAAa,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,SAAS,QAAQ,MAAM,+BAAA;GAC3E,IAAI,QAAQ,KAAK;IACf,MAAM,OAAO,aAAa,QAAQ,QAAQ,MAAM,CAAC;IACjD,IAAI,CAAC,MAAM,MAAM,+BAAA;SACZ;KACH,MAAM,OAAO;KACb,MAAM,QAAQ,QAAQ,QAAQ,OAAO,KAAK,KAAA;KAC1C,MAAM,MAAM;KACZ,MAAM,SAAS;KACf,MAAM,QAAQ,UAA+C;MAC3D,MAAM,eAAe;MACrB,IAAI,MAAM,SAAS,cAAc,MAAM,WAAW,GAAG;MACrD,SAAK,MAAM,aAAa,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC;KAC9C;KACA,MAAM,UAAU;KAChB,MAAM,aAAa;IACrB;GACF;GACA,IAAI,QAAQ,MAAM;IAChB,MAAM,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;IAC7C,IAAI,OAAO,cAAc,KAAK,KAAK,QAAQ,GAAG,MAAM,QAAQ;GAC9D;GACA,IAAI,QAAQ,QAAQ,QAAQ,MAAM;IAChC,MAAM,YAAY,OAAO,QAAQ,QAAQ,OAAO,KAAK,EAAE,CAAC,CAAC,MAAM,kCAAkC,CAAC,GAAG;IACrG,IAAI,WAAW,MAAM,QAAQ,EAAE,WAAW,UAAU;GACtD;GACA,OAAO,+BAAA,cAAc,KAAK,OAAO,GAAG,UAAU,QAAQ,CAAC;GACvD;EACF;EACA,QAAQ,MAAM,MAAd;GACE,KAAK;IACH,OAAO,oCAAA,IAAC,+BAAA,UAAD,EAAA,UAAuB,aAAa,MAAM,YAAY,CAAC,CAAC,EAAY,GAArD,KAAqD,CAAC;IAC5E;GACF,KAAK;IACH,OAAO,oCAAA,IAAC,QAAD,EAAA,UAAmB,MAAM,QAAc,GAA5B,KAA4B,CAAC;IAC/C;GACF,KAAK;GACL,KAAK;IACH,OAAO,oCAAA,IAAC,WAAD;KAAuB,UAAU,MAAM,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;KAAI,MAAM,MAAM;IAAU,GAA1E,KAA0E,CAAC;IAClG;GACF,KAAK;GACL,KAAK;IACH,OAAO,oCAAA,IAAC,MAAD,CAAiB,GAAR,KAAQ,CAAC;IACzB;GACF,KAAK;IACH,OAAO,oCAAA,IAAC,MAAD,CAAiB,GAAR,KAAQ,CAAC;IACzB;GACF,SACE,OAAO,oCAAA,IAAC,+BAAA,UAAD,EAAA,UAAuB,MAAM,QAAkB,GAAhC,KAAgC,CAAC;EAC3D;CACF,CAAC;CACD,OAAO,MAAM,EAAE,CAAC;AAClB;AAEA,SAAgB,mBAAmB,MAAkC;CACnE,MAAM,UAAU,KAAK,QAAQ,sBAAsB,EAAE;CAErD,IAAI,CAAC,QAAQ,KAAK,KAAK,4CAA4C,KAAK,OAAO,GAAG,OAAO,KAAA;CACzF,OAAO;AACT;AAEA,SAAS,oBAAwC;CAE/C,KAAK,MAAM,WAAW,SAAS,iBAA8B,wCAA4B,GAAG;EAC1F,IAAI,UAAU;EACd,KAAK,IAAI,OAA2B,SAAS,MAAM,OAAO,KAAK,eAAe;GAC5E,MAAM,QAAQ,iBAAiB,IAAI;GACnC,IACE,KAAK,UACL,KAAK,aAAa,aAAa,MAAM,UACrC,MAAM,YAAY,UAClB,MAAM,eAAe,YACrB,MAAM,eAAe,YACrB;IACA,UAAU;IACV;GACF;EACF;EACA,IAAI,CAAC,SAAS;EACd,MAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM,EAAa;EACtD,IAAI,CAAC,MAAM,CAAC,mBAAA,SAAS,UAAU,cAAc,YAAY,EAAE,GAAG;EAC9D,MAAM,MAAM,mBAAA,SAAS,UAAU,cAAc,eAAe,EAAE;EAC9D,IAAI,KAAK,WAAW,IAAI,YAAY,GAAG,OAAO;CAChD;AAEF;AAEA,SAAS,sBAA0C;CACjD,MAAM,CAAC,YAAY,iBAAiB,+BAAA,SAAiB;CACrD,+BAAA,gBAAgB;EACd,IAAI;EACJ,MAAM,eAAe;GACnB,OAAO;GACP,OAAO,8BAAA,cAAc,cAAc,kBAAkB,CAAC,CAAC;EACzD;EACA,OAAO;EACP,MAAM,WAAW,IAAI,iBAAiB,MAAM;EAC5C,SAAS,QAAQ,SAAS,MAAM;GAC9B,WAAW;GACX,SAAS;GACT,YAAY;GACZ,iBAAiB;IAAC;IAAS;IAAS;IAAU;IAAe;GAAa;EAC5E,CAAC;EACD,aAAa;GACX,SAAS,WAAW;GACpB,OAAO;EACT;CACF,GAAG,CAAC,CAAC;CACL,OAAO;AACT;AAEA,SAAS,UAAU,EAAE,MAAM,YAAgD;CACzE,MAAM,CAAC,UAAU,eAAe,+BAAA,SAAS,EAAE;CAC3C,MAAM,aAAa,oBAAoB;CACvC,MAAM,CAAC,WAAW,gBAAgB,+BAAA,SAAS,KAAK;CAChD,MAAM,QAAQ;EAAC;EAAI;EAAQ;EAAM;EAAS;EAAO;EAAW;CAAU,CAAC,CAAC,SAAS,SAAS,YAAY,CAAC;CACvG,MAAM,UAAU,mBAAmB,IAAI;CACvC,MAAM,SAAS,YAAY;EACzB,MAAM,iBAAiB,kBAAkB;EACzC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,kBAAkB,WAAW;EACxD,aAAa,IAAI;EACjB,IAAI;GACF,MAAM,mBAAA,SAAS,UAAU,cAAc,YAAY,SAAS;IAAE,OAAO;IAAO,OAAO;GAAe,CAAC;GACnG,YAAY,+DAA+D;EAC7E,QAAQ;GACN,YAAY,iCAAiC;EAC/C,UAAU;GACR,aAAa,KAAK;EACpB;CACF;CACA,OACE,oCAAA,KAAC,OAAD;EAAK,WAAU;YAAf;GACE,oCAAA,KAAC,OAAD;IAAK,WAAU;cAAf,CACE,oCAAA,IAAC,QAAD,EAAA,UAAO,YAAY,OAAa,CAAA,GAChC,oCAAA,KAAC,OAAD,EAAA,UAAA,CACE,oCAAA,KAAC,UAAD;KACE,MAAK;KACL,cAAW;KACX,OAAM;KACN,eAAe;MACb,IAAI;OACF,SAAA,UAAU,UAAU,IAAI;OACxB,YAAY,QAAQ;MACtB,QAAQ;OACN,YAAY,sBAAsB;MACpC;KACF;eAXF,CAaE,oCAAA,IAAC,gBAAA,QAAD;MAAQ,UAAS;MAAe,MAAM;KAAK,CAAA,GAAC,OACtC;QACP,SAAS,cACR,oCAAA,KAAC,UAAD;KACE,MAAK;KACL,cAAW;KACX,UAAU,CAAC,WAAW,CAAC,cAAc;KACrC,OACE,CAAC,UACG,kEACA,CAAC,aACC,yCACA;KAER,eAAe,KAAK,OAAO;eAX7B,CAaE,oCAAA,IAAC,gBAAA,QAAD;MAAQ,UAAS;MAAW,MAAM;KAAK,CAAA,GAAC,SAClC;MAEP,EAAA,CAAA,CACF;;GACL,oCAAA,IAAC,OAAD,EAAA,UACE,oCAAA,IAAC,QAAD,EAAA,UAAO,KAAW,CAAA,EACf,CAAA;GACJ,SAAS,CAAC,WACT,oCAAA,IAAC,OAAD;IAAK,WAAU;cAAqB;GAA0E,CAAA;GAEhH,oCAAA,IAAC,OAAD;IAAK,MAAK;IAAS,WAAU;cAC1B;GACE,CAAA;EACF;;AAET;AAEA,SAAgB,iBAAiB,EAAE,QAA0B;CAE3D,OAAO,oCAAA,IAAC,OAAD;EAAK,WAAU;YADN,+BAAA,cAAc,aAAa,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAChC;CAAa,CAAA;AAC1D"}
1
+ {"version":3,"file":"ai-message.js","names":[],"sources":["../../src/renderer/ai-message.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { clipboard, shell } from \"electron\";\nimport MarkdownIt, { type Token } from \"markdown-it\";\nimport { autorun } from \"mobx\";\nimport { createElement, Fragment, type ReactNode, useEffect, useMemo, useState } from \"react\";\nimport { AiIcon } from \"./ai-icon\";\nimport { isPowerLogsWorkspace, usePowerLogsWorkspace } from \"./ai-logs-sql\";\n\nconst markdown = new MarkdownIt({ html: false, linkify: true, breaks: true });\nconst markdownTags = new Set([\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"ul\",\n \"ol\",\n \"li\",\n \"blockquote\",\n \"table\",\n \"thead\",\n \"tbody\",\n \"tr\",\n \"th\",\n \"td\",\n \"strong\",\n \"em\",\n \"s\",\n \"a\",\n]);\n\nfunction externalLink(value: string | number | null): string | undefined {\n if (typeof value !== \"string\" || !value) return;\n try {\n const url = new URL(value);\n if ([\"https:\", \"http:\", \"mailto:\"].includes(url.protocol) && !url.username && !url.password) return url.href;\n } catch {}\n return;\n}\n\nfunction renderTokens(tokens: Token[]): ReactNode[] {\n type Container = { token?: Token; key: number; children: ReactNode[] };\n const stack: Container[] = [{ key: 0, children: [] }];\n const append = (node: ReactNode) => stack[stack.length - 1].children.push(node);\n tokens.forEach((token, index) => {\n if (token.nesting === 1) {\n stack.push({ token, key: index, children: [] });\n return;\n }\n if (token.nesting === -1) {\n if (stack.length === 1) return;\n const container = stack.pop()!;\n const opening = container.token!;\n const props: Record<string, unknown> = { key: container.key };\n let tag = markdownTags.has(opening.tag) && !opening.hidden ? opening.tag : Fragment;\n if (tag === \"a\") {\n const href = externalLink(opening.attrGet(\"href\"));\n if (!href) tag = Fragment;\n else {\n props.href = href;\n props.title = opening.attrGet(\"title\") ?? undefined;\n props.rel = \"noopener noreferrer\";\n props.target = \"_blank\";\n const open = (event: React.MouseEvent<HTMLAnchorElement>) => {\n event.preventDefault();\n if (event.type === \"auxclick\" && event.button !== 1) return;\n void shell.openExternal(href).catch(() => {});\n };\n props.onClick = open;\n props.onAuxClick = open;\n }\n }\n if (tag === \"ol\") {\n const start = Number(opening.attrGet(\"start\"));\n if (Number.isSafeInteger(start) && start > 0) props.start = start;\n }\n if (tag === \"th\" || tag === \"td\") {\n const alignment = String(opening.attrGet(\"style\") ?? \"\").match(/^text-align:(left|right|center)$/)?.[1];\n if (alignment) props.style = { textAlign: alignment };\n }\n append(createElement(tag, props, ...container.children));\n return;\n }\n switch (token.type) {\n case \"inline\":\n append(<Fragment key={index}>{renderTokens(token.children ?? [])}</Fragment>);\n break;\n case \"code_inline\":\n append(<code key={index}>{token.content}</code>);\n break;\n case \"fence\":\n case \"code_block\":\n append(<CodeBlock key={index} language={token.info.trim().split(/\\s+/)[0]} text={token.content} />);\n break;\n case \"hardbreak\":\n case \"softbreak\":\n append(<br key={index} />);\n break;\n case \"hr\":\n append(<hr key={index} />);\n break;\n default:\n append(<Fragment key={index}>{token.content}</Fragment>);\n }\n });\n return stack[0].children;\n}\n\nexport function terminalInsertText(text: string): string | undefined {\n const command = text.replace(/^[\\r\\n]+|[\\r\\n]+$/g, \"\");\n // surrounding line endings are formatting, embedded control characters must never reach the terminal\n if (!command.trim() || /[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/u.test(command)) return undefined;\n return command;\n}\n\nfunction visibleTerminalId(): string | undefined {\n const prefix = \"dock-tab-content-for-\";\n for (const element of document.querySelectorAll<HTMLElement>(`[data-testid^=\"${prefix}\"]`)) {\n let visible = true;\n for (let node: HTMLElement | null = element; node; node = node.parentElement) {\n const style = getComputedStyle(node);\n if (\n node.hidden ||\n node.getAttribute(\"aria-hidden\") === \"true\" ||\n style.display === \"none\" ||\n style.visibility === \"hidden\" ||\n style.visibility === \"collapse\"\n ) {\n visible = false;\n break;\n }\n }\n if (!visible) continue;\n const id = element.dataset.testid?.slice(prefix.length);\n if (!id || !Renderer.Component.terminalStore.isConnected(id)) continue;\n const api = Renderer.Component.terminalStore.getTerminalApi(id);\n if (api?.isReady && api.isConnected()) return id;\n }\n return;\n}\n\nfunction useActiveTerminalId(): string | undefined {\n const [terminalId, setTerminalId] = useState<string>();\n useEffect(() => {\n let stop: (() => void) | undefined;\n const update = () => {\n stop?.();\n stop = autorun(() => setTerminalId(visibleTerminalId()));\n };\n update();\n const observer = new MutationObserver(update);\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"hidden\", \"aria-hidden\", \"data-testid\"],\n });\n return () => {\n observer.disconnect();\n stop?.();\n };\n }, []);\n return terminalId;\n}\n\nfunction CodeBlock({ text, language }: { text: string; language: string }) {\n const [feedback, setFeedback] = useState(\"\");\n const terminalId = useActiveTerminalId();\n const [inserting, setInserting] = useState(false);\n const shell = [\"\", \"bash\", \"sh\", \"shell\", \"zsh\", \"console\", \"terminal\"].includes(language.toLowerCase());\n const command = terminalInsertText(text);\n const logsWorkspace = usePowerLogsWorkspace();\n const sqlDraft = [\"sql\", \"sqlite\"].includes(language.toLowerCase()) && logsWorkspace;\n const insert = async () => {\n const activeTerminal = visibleTerminalId();\n if (!shell || !command || !activeTerminal || inserting) return;\n setInserting(true);\n try {\n await Renderer.Component.terminalStore.sendCommand(command, { enter: false, tabId: activeTerminal });\n setFeedback(\"Inserted into terminal. Review the command before running it.\");\n } catch {\n setFeedback(\"Unable to insert into terminal.\");\n } finally {\n setInserting(false);\n }\n };\n return (\n <div className=\"power-ai-code\">\n <div className=\"power-ai-code-toolbar\">\n <span>{language || \"code\"}</span>\n <div>\n <button\n type=\"button\"\n aria-label=\"Copy code\"\n title=\"Copy code to clipboard\"\n onClick={() => {\n try {\n clipboard.writeText(text);\n setFeedback(\"Copied\");\n } catch {\n setFeedback(\"Unable to copy code.\");\n }\n }}\n >\n <AiIcon material=\"content_copy\" size={14} /> Copy\n </button>\n {sqlDraft && (\n <button\n type=\"button\"\n aria-label=\"Insert into SQL\"\n title=\"Insert into the SQL editor without running\"\n disabled={!text.trim() || text.length > 65536}\n onClick={() => {\n if (!isPowerLogsWorkspace() || !text.trim() || text.length > 65536) return;\n window.dispatchEvent(new CustomEvent(\"power-logs-sql-draft\", { detail: { sql: text } }));\n window.dispatchEvent(new Event(\"power-ai-close\"));\n }}\n >\n <AiIcon material=\"post_add\" size={14} /> Insert into SQL\n </button>\n )}\n {shell && terminalId && (\n <button\n type=\"button\"\n aria-label=\"Insert into terminal\"\n disabled={!command || !terminalId || inserting}\n title={\n !command\n ? \"Only a single line without control characters can be inserted\"\n : !terminalId\n ? \"Focus an open terminal tab to insert\"\n : \"Insert without running\"\n }\n onClick={() => void insert()}\n >\n <AiIcon material=\"terminal\" size={14} /> Insert\n </button>\n )}\n </div>\n </div>\n <pre>\n <code>{text}</code>\n </pre>\n {shell && !command && (\n <div className=\"power-ai-code-hint\">Terminal insertion requires a single line without control characters.</div>\n )}\n <div role=\"status\" className=\"power-ai-code-feedback\">\n {feedback}\n </div>\n </div>\n );\n}\n\nexport function AiMessageContent({ text }: { text: string }) {\n const content = useMemo(() => renderTokens(markdown.parse(text, {})), [text]);\n return <div className=\"power-ai-markdown\">{content}</div>;\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ const require_ai_icon = require("./ai-icon.js");
5
5
  const require_ai_terminal_context = require("./src/common/ai-terminal-context.js");
6
6
  const require_date_time = require("./src/common/date-time.js");
7
7
  //#region src/renderer/ai-terminal-context.tsx
8
+ var nextTerminalTooltip = 0;
8
9
  function activeAiTerminal(clusterId) {
9
10
  if (!clusterId || require_extensions.Renderer.Catalog.activeCluster.get()?.metadata.uid !== clusterId) return;
10
11
  for (const element of document.querySelectorAll(`[data-testid^="dock-tab-content-for-"]`)) {
@@ -51,6 +52,7 @@ function captureAiTerminal(clusterId, expectedId) {
51
52
  }
52
53
  }
53
54
  function AiTerminalContext({ clusterId, attachment, onChange, disabled }) {
55
+ const [tooltipId] = require__global_external_react.useState(() => `power-terminal-tooltip-${++nextTerminalTooltip}`);
54
56
  const [terminalId, setTerminalId] = require__global_external_react.useState(() => activeAiTerminal(clusterId));
55
57
  const [preview, setPreview] = require__global_external_react.useState();
56
58
  const [unavailable, setUnavailable] = require__global_external_react.useState(false);
@@ -108,50 +110,58 @@ function AiTerminalContext({ clusterId, attachment, onChange, disabled }) {
108
110
  clusterId,
109
111
  onChange
110
112
  ]);
113
+ const tooltip = attached ? "Detach terminal output from this question" : terminalId ? "Attach terminal output to this question" : "Open a connected terminal in this cluster";
111
114
  return /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
112
115
  className: "power-ai-terminal-context",
113
116
  style: { fontSize: 11 },
114
117
  children: [
115
- /* @__PURE__ */ require_jsx_runtime.jsx("button", {
116
- type: "button",
117
- "aria-label": "Attach terminal output",
118
- "aria-pressed": attached,
119
- style: {
120
- position: "absolute",
121
- top: 8,
122
- right: 8,
123
- display: "inline-flex",
124
- alignItems: "center",
125
- justifyContent: "center",
126
- width: 26,
127
- height: 26,
128
- minHeight: 26,
129
- padding: 0,
130
- color: "var(--textColorSecondary)",
131
- borderColor: "var(--borderFaintColor)",
132
- background: attached ? "color-mix(in srgb, var(--textColorPrimary) 8%, transparent)" : "transparent"
133
- },
134
- disabled: disabled || !terminalId,
135
- title: attached ? "Detach terminal output from this question" : terminalId ? "Attach terminal output to this question" : "Open a connected terminal in this cluster",
136
- onClick: () => {
137
- if (attached) {
138
- onChange(void 0);
139
- setPreview(void 0);
140
- return;
141
- }
142
- const snapshot = captureAiTerminal(clusterId, terminalId);
143
- setUnavailable(!snapshot);
144
- if (!snapshot || !clusterId || !terminalId) return;
145
- setPreview(snapshot);
146
- onChange({
147
- clusterId,
148
- terminalId
149
- });
150
- },
151
- children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
152
- material: "terminal",
153
- size: 16
154
- })
118
+ /* @__PURE__ */ require_jsx_runtime.jsx("style", { children: `.power-ai-terminal-trigger{position:absolute;top:8px;right:8px;display:inline-flex}.power-ai-terminal-tooltip{position:absolute;right:0;bottom:calc(100% + 8px);z-index:20;width:max-content;max-width:240px;padding:6px 8px;border:1px solid var(--borderFaintColor);border-radius:4px;background:var(--mainBackground);color:var(--textColorPrimary);font-size:12px;line-height:1.4;visibility:hidden;opacity:0;pointer-events:none}.power-ai-terminal-trigger:hover .power-ai-terminal-tooltip,.power-ai-terminal-trigger:focus-within .power-ai-terminal-tooltip{visibility:visible;opacity:1}` }),
119
+ /* @__PURE__ */ require_jsx_runtime.jsxs("span", {
120
+ className: "power-ai-terminal-trigger",
121
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx("button", {
122
+ type: "button",
123
+ "aria-describedby": tooltipId,
124
+ "aria-label": "Attach terminal output",
125
+ "aria-pressed": attached,
126
+ style: {
127
+ display: "inline-flex",
128
+ alignItems: "center",
129
+ justifyContent: "center",
130
+ width: 26,
131
+ height: 26,
132
+ minHeight: 26,
133
+ padding: 0,
134
+ color: "var(--textColorSecondary)",
135
+ borderColor: "var(--borderFaintColor)",
136
+ background: attached ? "color-mix(in srgb, var(--textColorPrimary) 8%, transparent)" : "transparent"
137
+ },
138
+ disabled: disabled || !terminalId,
139
+ title: tooltip,
140
+ onClick: () => {
141
+ if (attached) {
142
+ onChange(void 0);
143
+ setPreview(void 0);
144
+ return;
145
+ }
146
+ const snapshot = captureAiTerminal(clusterId, terminalId);
147
+ setUnavailable(!snapshot);
148
+ if (!snapshot || !clusterId || !terminalId) return;
149
+ setPreview(snapshot);
150
+ onChange({
151
+ clusterId,
152
+ terminalId
153
+ });
154
+ },
155
+ children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
156
+ material: "terminal",
157
+ size: 16
158
+ })
159
+ }), /* @__PURE__ */ require_jsx_runtime.jsx("span", {
160
+ role: "tooltip",
161
+ id: tooltipId,
162
+ className: "power-ai-terminal-tooltip",
163
+ children: tooltip
164
+ })]
155
165
  }),
156
166
  attached && preview && /* @__PURE__ */ require_jsx_runtime.jsxs("details", {
157
167
  style: {
@@ -1 +1 @@
1
- {"version":3,"file":"ai-terminal-context.js","names":[],"sources":["../../src/renderer/ai-terminal-context.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { useEffect, useState } from \"react\";\nimport { type AiTerminalSnapshot, sanitizeAiTerminalSnapshot } from \"../common/ai-terminal-context\";\nimport { formatDateTime } from \"../common/date-time\";\nimport { AiIcon } from \"./ai-icon\";\n\ntype TerminalBuffer = {\n length: number;\n baseY: number;\n cursorY: number;\n getLine(index: number): { isWrapped?: boolean; translateToString(trimRight?: boolean): string } | undefined;\n};\n\nexport function activeAiTerminal(clusterId?: string): string | undefined {\n if (!clusterId || Renderer.Catalog.activeCluster.get()?.metadata.uid !== clusterId) return;\n const prefix = \"dock-tab-content-for-\";\n for (const element of document.querySelectorAll<HTMLElement>(`[data-testid^=\"${prefix}\"]`)) {\n let visible = true;\n for (let node: HTMLElement | null = element; node; node = node.parentElement) {\n const style = getComputedStyle(node);\n if (\n node.hidden ||\n node.getAttribute(\"aria-hidden\") === \"true\" ||\n style.display === \"none\" ||\n style.visibility === \"hidden\" ||\n style.visibility === \"collapse\"\n ) {\n visible = false;\n break;\n }\n }\n const id = element.dataset.testid?.slice(prefix.length);\n if (!visible || !id || !Renderer.Component.terminalStore.isConnected(id)) continue;\n const api = Renderer.Component.terminalStore.getTerminalApi(id);\n if (api?.isReady && api.isConnected()) return id;\n }\n return;\n}\n\nexport function captureAiTerminal(clusterId?: string, expectedId?: string): AiTerminalSnapshot | undefined {\n const id = activeAiTerminal(clusterId);\n if (!id || (expectedId && id !== expectedId)) return;\n try {\n // [xterm] is a guarded host implementation detail with no public buffer accessor\n const terminal = Renderer.Component.terminalStore.getTerminal(id) as unknown as\n | {\n xterm?: { buffer?: { active?: TerminalBuffer } };\n }\n | undefined;\n const buffer = terminal?.xterm?.buffer?.active;\n if (!buffer || !Number.isSafeInteger(buffer.length) || buffer.length > 10_000 || buffer.length < 0) return;\n const end = buffer.length;\n if (!Number.isSafeInteger(end) || end < 0) return;\n const logical: string[] = [];\n let characters = 0;\n for (let index = 0; index < end; index++) {\n const line = buffer.getLine(index);\n if (!line || (index === 0 && line.isWrapped)) return;\n const text = line.translateToString(!buffer.getLine(index + 1)?.isWrapped);\n characters += text.length + 1;\n if (characters > 1_000_000) return;\n if (line.isWrapped && logical.length) logical[logical.length - 1] += text;\n else logical.push(text);\n }\n if (activeAiTerminal(clusterId) !== id) return;\n return sanitizeAiTerminalSnapshot({ text: logical.join(\"\\n\"), capturedAt: new Date().toISOString() });\n } catch {\n return;\n }\n}\n\nexport interface AiTerminalAttachment {\n clusterId: string;\n terminalId: string;\n}\n\nexport function AiTerminalContext({\n clusterId,\n attachment,\n onChange,\n disabled,\n}: {\n clusterId?: string;\n attachment?: AiTerminalAttachment;\n onChange(value: AiTerminalAttachment | undefined): void;\n disabled: boolean;\n}) {\n const [terminalId, setTerminalId] = useState(() => activeAiTerminal(clusterId));\n const [preview, setPreview] = useState<AiTerminalSnapshot>();\n const [unavailable, setUnavailable] = useState(false);\n useEffect(() => {\n const update = () => setTerminalId(activeAiTerminal(clusterId));\n update();\n const timer = window.setInterval(update, 500);\n return () => window.clearInterval(timer);\n }, [clusterId]);\n const attached = Boolean(attachment && attachment.clusterId === clusterId && attachment.terminalId === terminalId);\n useEffect(() => {\n if (!attached) {\n if (attachment) onChange(undefined);\n setPreview(undefined);\n return;\n }\n let stopped = false;\n let scheduled: number | undefined;\n let polling: number | undefined;\n let subscription: { dispose(): void } | undefined;\n const stop = () => {\n stopped = true;\n window.clearTimeout(scheduled);\n window.clearInterval(polling);\n subscription?.dispose();\n subscription = undefined;\n };\n const update = () => {\n if (stopped) return;\n scheduled = undefined;\n const snapshot = captureAiTerminal(clusterId, attachment?.terminalId);\n setUnavailable(!snapshot);\n if (!snapshot) {\n setPreview(undefined);\n if (activeAiTerminal(clusterId) !== attachment?.terminalId) {\n stop();\n onChange(undefined);\n }\n return;\n }\n setPreview((previous) =>\n previous?.text === snapshot.text && previous.truncated === snapshot.truncated ? previous : snapshot,\n );\n };\n update();\n if (stopped) return stop;\n try {\n const terminal = Renderer.Component.terminalStore.getTerminal(attachment!.terminalId) as unknown as\n | {\n xterm?: { onWriteParsed?: (listener: () => void) => { dispose(): void } };\n }\n | undefined;\n subscription = terminal?.xterm?.onWriteParsed?.(() => {\n if (!stopped && scheduled === undefined) scheduled = window.setTimeout(update, 250);\n });\n } catch {}\n if (!subscription) polling = window.setInterval(update, 1000);\n return stop;\n }, [attached, attachment, clusterId, onChange]);\n return (\n <div className=\"power-ai-terminal-context\" style={{ fontSize: 11 }}>\n <button\n type=\"button\"\n aria-label=\"Attach terminal output\"\n aria-pressed={attached}\n style={{\n position: \"absolute\",\n top: 8,\n right: 8,\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n minHeight: 26,\n padding: 0,\n color: \"var(--textColorSecondary)\",\n borderColor: \"var(--borderFaintColor)\",\n background: attached ? \"color-mix(in srgb, var(--textColorPrimary) 8%, transparent)\" : \"transparent\",\n }}\n disabled={disabled || !terminalId}\n title={\n attached\n ? \"Detach terminal output from this question\"\n : terminalId\n ? \"Attach terminal output to this question\"\n : \"Open a connected terminal in this cluster\"\n }\n onClick={() => {\n if (attached) {\n onChange(undefined);\n setPreview(undefined);\n return;\n }\n const snapshot = captureAiTerminal(clusterId, terminalId);\n setUnavailable(!snapshot);\n if (!snapshot || !clusterId || !terminalId) return;\n setPreview(snapshot);\n onChange({ clusterId, terminalId });\n }}\n >\n <AiIcon material=\"terminal\" size={16} />\n </button>\n {attached && preview && (\n <details style={{ padding: \"0 16px 4px\", margin: 0 }}>\n <summary>\n Terminal · {preview.text.split(\"\\n\").length} rows{preview.truncated ? \" · truncated\" : \"\"}\n </summary>\n <p>\n Preview captured <span style={{ whiteSpace: \"pre-wrap\" }}>{formatDateTime(preview.capturedAt)}</span> ·\n captured again when sent · up to 300 rows / 32 KiB · Common credentials masked. Review before sending.\n </p>\n <pre style={{ maxHeight: 160, overflow: \"auto\", whiteSpace: \"pre-wrap\" }}>{preview.text}</pre>\n <p>The attachment is not stored in chat history. Assistant replies may quote it.</p>\n </details>\n )}\n {unavailable && (\n <div role=\"status\" style={{ padding: \"4px 16px 8px\" }}>\n Terminal output is unavailable or exceeds the safe capture limit\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-terminal-context.js","names":[],"sources":["../../src/renderer/ai-terminal-context.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { useEffect, useState } from \"react\";\nimport { type AiTerminalSnapshot, sanitizeAiTerminalSnapshot } from \"../common/ai-terminal-context\";\nimport { formatDateTime } from \"../common/date-time\";\nimport { AiIcon } from \"./ai-icon\";\n\nlet nextTerminalTooltip = 0;\n\ntype TerminalBuffer = {\n length: number;\n baseY: number;\n cursorY: number;\n getLine(index: number): { isWrapped?: boolean; translateToString(trimRight?: boolean): string } | undefined;\n};\n\nexport function activeAiTerminal(clusterId?: string): string | undefined {\n if (!clusterId || Renderer.Catalog.activeCluster.get()?.metadata.uid !== clusterId) return;\n const prefix = \"dock-tab-content-for-\";\n for (const element of document.querySelectorAll<HTMLElement>(`[data-testid^=\"${prefix}\"]`)) {\n let visible = true;\n for (let node: HTMLElement | null = element; node; node = node.parentElement) {\n const style = getComputedStyle(node);\n if (\n node.hidden ||\n node.getAttribute(\"aria-hidden\") === \"true\" ||\n style.display === \"none\" ||\n style.visibility === \"hidden\" ||\n style.visibility === \"collapse\"\n ) {\n visible = false;\n break;\n }\n }\n const id = element.dataset.testid?.slice(prefix.length);\n if (!visible || !id || !Renderer.Component.terminalStore.isConnected(id)) continue;\n const api = Renderer.Component.terminalStore.getTerminalApi(id);\n if (api?.isReady && api.isConnected()) return id;\n }\n return;\n}\n\nexport function captureAiTerminal(clusterId?: string, expectedId?: string): AiTerminalSnapshot | undefined {\n const id = activeAiTerminal(clusterId);\n if (!id || (expectedId && id !== expectedId)) return;\n try {\n // [xterm] is a guarded host implementation detail with no public buffer accessor\n const terminal = Renderer.Component.terminalStore.getTerminal(id) as unknown as\n | {\n xterm?: { buffer?: { active?: TerminalBuffer } };\n }\n | undefined;\n const buffer = terminal?.xterm?.buffer?.active;\n if (!buffer || !Number.isSafeInteger(buffer.length) || buffer.length > 10_000 || buffer.length < 0) return;\n const end = buffer.length;\n if (!Number.isSafeInteger(end) || end < 0) return;\n const logical: string[] = [];\n let characters = 0;\n for (let index = 0; index < end; index++) {\n const line = buffer.getLine(index);\n if (!line || (index === 0 && line.isWrapped)) return;\n const text = line.translateToString(!buffer.getLine(index + 1)?.isWrapped);\n characters += text.length + 1;\n if (characters > 1_000_000) return;\n if (line.isWrapped && logical.length) logical[logical.length - 1] += text;\n else logical.push(text);\n }\n if (activeAiTerminal(clusterId) !== id) return;\n return sanitizeAiTerminalSnapshot({ text: logical.join(\"\\n\"), capturedAt: new Date().toISOString() });\n } catch {\n return;\n }\n}\n\nexport interface AiTerminalAttachment {\n clusterId: string;\n terminalId: string;\n}\n\nexport function AiTerminalContext({\n clusterId,\n attachment,\n onChange,\n disabled,\n}: {\n clusterId?: string;\n attachment?: AiTerminalAttachment;\n onChange(value: AiTerminalAttachment | undefined): void;\n disabled: boolean;\n}) {\n const [tooltipId] = useState(() => `power-terminal-tooltip-${++nextTerminalTooltip}`);\n const [terminalId, setTerminalId] = useState(() => activeAiTerminal(clusterId));\n const [preview, setPreview] = useState<AiTerminalSnapshot>();\n const [unavailable, setUnavailable] = useState(false);\n useEffect(() => {\n const update = () => setTerminalId(activeAiTerminal(clusterId));\n update();\n const timer = window.setInterval(update, 500);\n return () => window.clearInterval(timer);\n }, [clusterId]);\n const attached = Boolean(attachment && attachment.clusterId === clusterId && attachment.terminalId === terminalId);\n useEffect(() => {\n if (!attached) {\n if (attachment) onChange(undefined);\n setPreview(undefined);\n return;\n }\n let stopped = false;\n let scheduled: number | undefined;\n let polling: number | undefined;\n let subscription: { dispose(): void } | undefined;\n const stop = () => {\n stopped = true;\n window.clearTimeout(scheduled);\n window.clearInterval(polling);\n subscription?.dispose();\n subscription = undefined;\n };\n const update = () => {\n if (stopped) return;\n scheduled = undefined;\n const snapshot = captureAiTerminal(clusterId, attachment?.terminalId);\n setUnavailable(!snapshot);\n if (!snapshot) {\n setPreview(undefined);\n if (activeAiTerminal(clusterId) !== attachment?.terminalId) {\n stop();\n onChange(undefined);\n }\n return;\n }\n setPreview((previous) =>\n previous?.text === snapshot.text && previous.truncated === snapshot.truncated ? previous : snapshot,\n );\n };\n update();\n if (stopped) return stop;\n try {\n const terminal = Renderer.Component.terminalStore.getTerminal(attachment!.terminalId) as unknown as\n | {\n xterm?: { onWriteParsed?: (listener: () => void) => { dispose(): void } };\n }\n | undefined;\n subscription = terminal?.xterm?.onWriteParsed?.(() => {\n if (!stopped && scheduled === undefined) scheduled = window.setTimeout(update, 250);\n });\n } catch {}\n if (!subscription) polling = window.setInterval(update, 1000);\n return stop;\n }, [attached, attachment, clusterId, onChange]);\n const tooltip = attached\n ? \"Detach terminal output from this question\"\n : terminalId\n ? \"Attach terminal output to this question\"\n : \"Open a connected terminal in this cluster\";\n return (\n <div className=\"power-ai-terminal-context\" style={{ fontSize: 11 }}>\n <style>{`.power-ai-terminal-trigger{position:absolute;top:8px;right:8px;display:inline-flex}.power-ai-terminal-tooltip{position:absolute;right:0;bottom:calc(100% + 8px);z-index:20;width:max-content;max-width:240px;padding:6px 8px;border:1px solid var(--borderFaintColor);border-radius:4px;background:var(--mainBackground);color:var(--textColorPrimary);font-size:12px;line-height:1.4;visibility:hidden;opacity:0;pointer-events:none}.power-ai-terminal-trigger:hover .power-ai-terminal-tooltip,.power-ai-terminal-trigger:focus-within .power-ai-terminal-tooltip{visibility:visible;opacity:1}`}</style>\n <span className=\"power-ai-terminal-trigger\">\n <button\n type=\"button\"\n aria-describedby={tooltipId}\n aria-label=\"Attach terminal output\"\n aria-pressed={attached}\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n minHeight: 26,\n padding: 0,\n color: \"var(--textColorSecondary)\",\n borderColor: \"var(--borderFaintColor)\",\n background: attached ? \"color-mix(in srgb, var(--textColorPrimary) 8%, transparent)\" : \"transparent\",\n }}\n disabled={disabled || !terminalId}\n title={tooltip}\n onClick={() => {\n if (attached) {\n onChange(undefined);\n setPreview(undefined);\n return;\n }\n const snapshot = captureAiTerminal(clusterId, terminalId);\n setUnavailable(!snapshot);\n if (!snapshot || !clusterId || !terminalId) return;\n setPreview(snapshot);\n onChange({ clusterId, terminalId });\n }}\n >\n <AiIcon material=\"terminal\" size={16} />\n </button>\n <span role=\"tooltip\" id={tooltipId} className=\"power-ai-terminal-tooltip\">\n {tooltip}\n </span>\n </span>\n {attached && preview && (\n <details style={{ padding: \"0 16px 4px\", margin: 0 }}>\n <summary>\n Terminal · {preview.text.split(\"\\n\").length} rows{preview.truncated ? \" · truncated\" : \"\"}\n </summary>\n <p>\n Preview captured <span style={{ whiteSpace: \"pre-wrap\" }}>{formatDateTime(preview.capturedAt)}</span> ·\n captured again when sent · up to 300 rows / 32 KiB · Common credentials masked. Review before sending.\n </p>\n <pre style={{ maxHeight: 160, overflow: \"auto\", whiteSpace: \"pre-wrap\" }}>{preview.text}</pre>\n <p>The attachment is not stored in chat history. Assistant replies may quote it.</p>\n </details>\n )}\n {unavailable && (\n <div role=\"status\" style={{ padding: \"4px 16px 8px\" }}>\n Terminal output is unavailable or exceeds the safe capture limit\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -13,6 +13,7 @@ const require_ai_icon = require("./ai-icon.js");
13
13
  const require_ai_copy_answer = require("./ai-copy-answer.js");
14
14
  const require_ai_diagnostic_handoff = require("./ai-diagnostic-handoff.js");
15
15
  const require_ai_extras$1 = require("./ai-extras.js");
16
+ const require_ai_logs_sql = require("./ai-logs-sql.js");
16
17
  const require_ai_message = require("./ai-message.js");
17
18
  const require_ai_provider_icon = require("./ai-provider-icon.js");
18
19
  const require_ai_resize = require("./ai-resize.js");
@@ -306,9 +307,15 @@ var AiChat = require__global_external_mobx_react.observer(({ isClusterDocument }
306
307
  };
307
308
  window.addEventListener(require_ai_diagnostic_handoff.AI_DIAGNOSTIC_EVENT, onDiagnostic);
308
309
  window.addEventListener("power-ai-open", updateAndOpen);
309
- function updateAndOpen() {
310
+ window.addEventListener("power-ai-open-only", openOnly);
311
+ const closePanel = () => setOpen(false);
312
+ window.addEventListener("power-ai-close", closePanel);
313
+ function openOnly() {
310
314
  update();
311
315
  setOpen(true);
316
+ }
317
+ function updateAndOpen() {
318
+ openOnly();
312
319
  clusterReview.current();
313
320
  }
314
321
  return () => {
@@ -321,6 +328,8 @@ var AiChat = require__global_external_mobx_react.observer(({ isClusterDocument }
321
328
  window.clearInterval(timer);
322
329
  observer.disconnect();
323
330
  window.removeEventListener("power-ai-open", updateAndOpen);
331
+ window.removeEventListener("power-ai-open-only", openOnly);
332
+ window.removeEventListener("power-ai-close", closePanel);
324
333
  window.removeEventListener(require_ai_diagnostic_handoff.AI_DIAGNOSTIC_EVENT, onDiagnostic);
325
334
  };
326
335
  }, []);
@@ -465,7 +474,11 @@ var AiChat = require__global_external_mobx_react.observer(({ isClusterDocument }
465
474
  }, []),
466
475
  context: requestContext,
467
476
  ...terminalSnapshot ? { terminalSnapshot } : {},
468
- systemPrompt: [preferences.systemPrompt, requestContext.clusterId ? preferences.clusterPrompt : ""].filter(Boolean).join("\n\n"),
477
+ systemPrompt: [
478
+ preferences.systemPrompt,
479
+ requestContext.clusterId ? preferences.clusterPrompt : "",
480
+ require_ai_logs_sql.isPowerLogsWorkspace() ? require_ai_logs_sql.POWER_LOGS_SQL_DRAFT_PROMPT : ""
481
+ ].filter(Boolean).join("\n\n"),
469
482
  model: preferences.model,
470
483
  speed: preferences.speed,
471
484
  reasoningEffort: preferences.reasoningEffort