@64mb/freelens-power 1.5.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 (71) hide show
  1. package/README.md +4 -1
  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/store/logs-preferences-store.js +45 -0
  9. package/out/main/common/store/logs-preferences-store.js.map +1 -0
  10. package/out/main/dashboard-icon-cache.js +1 -1
  11. package/out/main/index.js +4 -0
  12. package/out/main/index.js.map +1 -1
  13. package/out/main/power-logs-budget.js +126 -0
  14. package/out/main/power-logs-budget.js.map +1 -0
  15. package/out/main/power-logs-child.js +86 -0
  16. package/out/main/power-logs-child.js.map +1 -0
  17. package/out/main/power-logs-database.js +179 -0
  18. package/out/main/power-logs-database.js.map +1 -0
  19. package/out/main/power-logs-ipc.js +275 -0
  20. package/out/main/power-logs-ipc.js.map +1 -0
  21. package/out/main/power-logs-query-child.js +23 -0
  22. package/out/main/power-logs-query-child.js.map +1 -0
  23. package/out/main/power-logs-query.js +127 -0
  24. package/out/main/power-logs-query.js.map +1 -0
  25. package/out/main/power-logs-worker.js +215 -0
  26. package/out/main/power-logs-worker.js.map +1 -0
  27. package/out/main/resource-search.js +1 -1
  28. package/out/renderer/ai-logs-sql.js +27 -0
  29. package/out/renderer/ai-logs-sql.js.map +1 -0
  30. package/out/renderer/ai-message.js +48 -27
  31. package/out/renderer/ai-message.js.map +1 -1
  32. package/out/renderer/ai-terminal-context.js +50 -40
  33. package/out/renderer/ai-terminal-context.js.map +1 -1
  34. package/out/renderer/ai.js +15 -2
  35. package/out/renderer/ai.js.map +1 -1
  36. package/out/renderer/appearance.js +1 -0
  37. package/out/renderer/appearance.js.map +1 -1
  38. package/out/renderer/index.js +67 -29
  39. package/out/renderer/index.js.map +1 -1
  40. package/out/renderer/power-log-json.js +29 -0
  41. package/out/renderer/power-log-json.js.map +1 -0
  42. package/out/renderer/power-logs-client.js +35 -0
  43. package/out/renderer/power-logs-client.js.map +1 -0
  44. package/out/renderer/power-logs-collector.js +316 -0
  45. package/out/renderer/power-logs-collector.js.map +1 -0
  46. package/out/renderer/power-logs-entrypoints.js +201 -0
  47. package/out/renderer/power-logs-entrypoints.js.map +1 -0
  48. package/out/renderer/power-logs-sql-row.js +52 -0
  49. package/out/renderer/power-logs-sql-row.js.map +1 -0
  50. package/out/renderer/power-logs-transport.js +122 -0
  51. package/out/renderer/power-logs-transport.js.map +1 -0
  52. package/out/renderer/power-logs.css.js +116 -0
  53. package/out/renderer/power-logs.css.js.map +1 -0
  54. package/out/renderer/power-logs.js +1352 -0
  55. package/out/renderer/power-logs.js.map +1 -0
  56. package/out/renderer/preferences/logs-preferences.js +93 -0
  57. package/out/renderer/preferences/logs-preferences.js.map +1 -0
  58. package/out/renderer/preferences/preferences.js +23 -9
  59. package/out/renderer/preferences/preferences.js.map +1 -1
  60. package/out/renderer/resource-relationships.js +1 -1
  61. package/out/renderer/src/common/power-log-parser.js +114 -0
  62. package/out/renderer/src/common/power-log-parser.js.map +1 -0
  63. package/out/renderer/src/common/power-logs-ai.js +56 -0
  64. package/out/renderer/src/common/power-logs-ai.js.map +1 -0
  65. package/out/renderer/src/common/power-logs-limits.js +227 -0
  66. package/out/renderer/src/common/power-logs-limits.js.map +1 -0
  67. package/out/renderer/src/common/power-logs.js +32 -0
  68. package/out/renderer/src/common/power-logs.js.map +1 -0
  69. package/out/renderer/src/common/store/logs-preferences-store.js +45 -0
  70. package/out/renderer/src/common/store/logs-preferences-store.js.map +1 -0
  71. package/package.json +4 -2
@@ -0,0 +1,215 @@
1
+ const require_power_logs = require("./common/power-logs.js");
2
+ const require_power_logs_limits = require("./common/power-logs-limits.js");
3
+ let node_crypto = require("node:crypto");
4
+ let electron = require("electron");
5
+ let node_os = require("node:os");
6
+ let node_path = require("node:path");
7
+ let node_fs = require("node:fs");
8
+ //#region src/main/power-logs-worker.ts
9
+ function sweepPowerLogsStorage(directory = (0, node_path.join)((0, node_os.tmpdir)(), "freelens-power-logs")) {
10
+ (0, node_fs.mkdirSync)(directory, {
11
+ recursive: true,
12
+ mode: 448
13
+ });
14
+ const root = (0, node_fs.lstatSync)(directory);
15
+ if (!root.isDirectory() || root.isSymbolicLink() || process.getuid && root.uid !== process.getuid()) throw new Error("Log storage unavailable");
16
+ (0, node_fs.chmodSync)(directory, 448);
17
+ for (const name of (0, node_fs.readdirSync)(directory)) {
18
+ const match = /^([1-9][0-9]*)-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/.exec(name);
19
+ if (!match) continue;
20
+ const pid = Number(match[1]);
21
+ if (!Number.isSafeInteger(pid) || pid > 2147483647) continue;
22
+ const path = (0, node_path.join)(directory, name);
23
+ try {
24
+ const entry = (0, node_fs.lstatSync)(path);
25
+ if (!entry.isDirectory() || entry.isSymbolicLink() || process.getuid && entry.uid !== process.getuid()) continue;
26
+ try {
27
+ process.kill(pid, 0);
28
+ } catch (error) {
29
+ if (error.code === "ESRCH") (0, node_fs.rmSync)(path, {
30
+ recursive: true,
31
+ force: true
32
+ });
33
+ }
34
+ } catch {}
35
+ }
36
+ return directory;
37
+ }
38
+ var PowerLogsWorker = class {
39
+ child;
40
+ directory;
41
+ pending = /* @__PURE__ */ new Map();
42
+ sequence = 0;
43
+ closed = false;
44
+ children = /* @__PURE__ */ new Set();
45
+ query;
46
+ readers = /* @__PURE__ */ new Map();
47
+ deleting = /* @__PURE__ */ new Set();
48
+ captureKey(request) {
49
+ return JSON.stringify([
50
+ request.clusterId,
51
+ request.contextId,
52
+ request.captureId
53
+ ]);
54
+ }
55
+ get isClosed() {
56
+ return this.closed;
57
+ }
58
+ constructor() {
59
+ this.directory = (0, node_path.join)(sweepPowerLogsStorage(), `${process.pid}-${(0, node_crypto.randomUUID)()}`);
60
+ (0, node_fs.mkdirSync)(this.directory, { mode: 448 });
61
+ try {
62
+ this.child = electron.utilityProcess.fork((0, node_path.join)(__dirname, "power-logs-child.js"), [this.directory], {
63
+ serviceName: "Power Logs",
64
+ stdio: "ignore"
65
+ });
66
+ } catch {
67
+ (0, node_fs.rmSync)(this.directory, {
68
+ recursive: true,
69
+ force: true
70
+ });
71
+ throw new Error("Log process unavailable");
72
+ }
73
+ this.track(this.child);
74
+ this.child.on("message", ({ id, result, error }) => {
75
+ const pending = this.pending.get(id);
76
+ if (!pending) return;
77
+ clearTimeout(pending.timer);
78
+ this.pending.delete(id);
79
+ if (error) pending.reject(/* @__PURE__ */ new Error("Log operation failed or exceeded its limit"));
80
+ else pending.resolve(result);
81
+ });
82
+ this.child.on("exit", () => {
83
+ this.close();
84
+ });
85
+ }
86
+ track(child) {
87
+ this.children.add(child);
88
+ child.once("exit", () => {
89
+ this.children.delete(child);
90
+ this.cleanup();
91
+ });
92
+ }
93
+ cleanup() {
94
+ if (this.closed && this.children.size === 0) (0, node_fs.rmSync)(this.directory, {
95
+ recursive: true,
96
+ force: true
97
+ });
98
+ }
99
+ call(request, destination) {
100
+ if (request.operation === "query") return this.runQuery(request);
101
+ const active = this.query;
102
+ if (active && request.clusterId === active.request.clusterId && request.contextId === active.request.contextId && (request.operation === "cancel" && request.queryId === active.request.queryId || request.operation === "delete" && request.captureId === active.request.captureId)) active.reject(/* @__PURE__ */ new Error("Log query canceled"));
103
+ if (request.operation === "delete") {
104
+ const key = this.captureKey(request);
105
+ this.deleting.add(key);
106
+ const readers = [...this.readers].filter(([, owner]) => this.captureKey(owner) === key);
107
+ if (readers.length === 0) return this.callStorage(request, destination).finally(() => this.deleting.delete(key));
108
+ return Promise.all(readers.map(([child]) => new Promise((resolve, reject) => {
109
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Log query process is still closing")), 5e3);
110
+ child.once("exit", () => {
111
+ clearTimeout(timer);
112
+ resolve();
113
+ });
114
+ child.kill();
115
+ }))).then(() => this.callStorage(request, destination)).finally(() => this.deleting.delete(key));
116
+ }
117
+ return this.callStorage(request, destination);
118
+ }
119
+ runQuery(request) {
120
+ if (this.closed || this.query || this.deleting.has(this.captureKey(request))) return Promise.reject(/* @__PURE__ */ new Error("Log query unavailable or busy"));
121
+ return new Promise((resolve, reject) => {
122
+ const finish = (error, result) => {
123
+ if (this.query !== active) return;
124
+ this.query = void 0;
125
+ clearTimeout(active.timer);
126
+ clearTimeout(active.expiry);
127
+ active.child?.kill();
128
+ if (error) reject(error);
129
+ else resolve(result);
130
+ };
131
+ const active = {
132
+ request,
133
+ reject: (error) => finish(error),
134
+ timer: setTimeout(() => finish(/* @__PURE__ */ new Error("Log query exceeded its time limit")), require_power_logs.POWER_LOGS_LIMITS.queryMs)
135
+ };
136
+ this.query = active;
137
+ this.callStorage(request).then((value) => {
138
+ if (this.query !== active) return;
139
+ const snapshot = value;
140
+ const local = typeof snapshot?.database === "string" ? (0, node_path.relative)(this.directory, snapshot.database) : "..";
141
+ if (!local || local.startsWith("..") || (0, node_path.isAbsolute)(local) || snapshot.captureId !== request.captureId || !Number.isFinite(snapshot.expiresAt) || snapshot.expiresAt <= Date.now()) {
142
+ finish(/* @__PURE__ */ new Error("Log snapshot unavailable"));
143
+ return;
144
+ }
145
+ try {
146
+ const child = electron.utilityProcess.fork((0, node_path.join)(__dirname, "power-logs-query-child.js"), [], {
147
+ serviceName: "Power Logs SQL",
148
+ stdio: "ignore"
149
+ });
150
+ active.child = child;
151
+ this.track(child);
152
+ this.readers.set(child, request);
153
+ child.once("exit", () => this.readers.delete(child));
154
+ active.expiry = setTimeout(() => finish(/* @__PURE__ */ new Error("Log snapshot expired")), snapshot.expiresAt - Date.now());
155
+ child.once("exit", () => {
156
+ clearTimeout(active.expiry);
157
+ finish(/* @__PURE__ */ new Error("Log query process unavailable"));
158
+ });
159
+ child.once("message", ({ id, result, error }) => {
160
+ if (id !== 1) return;
161
+ clearTimeout(active.expiry);
162
+ finish(error ? /* @__PURE__ */ new Error("Log query failed or exceeded its limit") : void 0, result);
163
+ });
164
+ child.postMessage({
165
+ id: 1,
166
+ database: snapshot.database,
167
+ sql: request.sql,
168
+ limits: require_power_logs_limits.normalizePowerLogsLimits(require_power_logs.POWER_LOGS_LIMITS)
169
+ });
170
+ } catch {
171
+ finish(/* @__PURE__ */ new Error("Log query process unavailable"));
172
+ }
173
+ }, (error) => finish(error));
174
+ });
175
+ }
176
+ callStorage(request, destination) {
177
+ if (this.closed || this.pending.size >= 16) return Promise.reject(/* @__PURE__ */ new Error("Log storage unavailable or busy"));
178
+ return new Promise((resolve, reject) => {
179
+ const id = ++this.sequence;
180
+ const timer = setTimeout(() => this.close(), 2e4);
181
+ this.pending.set(id, {
182
+ resolve,
183
+ reject,
184
+ timer
185
+ });
186
+ try {
187
+ this.child.postMessage({
188
+ id,
189
+ request,
190
+ destination,
191
+ limits: require_power_logs_limits.normalizePowerLogsLimits(require_power_logs.POWER_LOGS_LIMITS)
192
+ });
193
+ } catch {
194
+ this.close();
195
+ }
196
+ });
197
+ }
198
+ close() {
199
+ if (this.closed) return;
200
+ this.closed = true;
201
+ this.query?.reject(/* @__PURE__ */ new Error("Log storage unavailable"));
202
+ for (const child of this.children) child.kill();
203
+ for (const pending of this.pending.values()) {
204
+ clearTimeout(pending.timer);
205
+ pending.reject(/* @__PURE__ */ new Error("Log storage unavailable"));
206
+ }
207
+ this.pending.clear();
208
+ this.cleanup();
209
+ }
210
+ };
211
+ //#endregion
212
+ exports.PowerLogsWorker = PowerLogsWorker;
213
+ exports.sweepPowerLogsStorage = sweepPowerLogsStorage;
214
+
215
+ //# sourceMappingURL=power-logs-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power-logs-worker.js","names":[],"sources":["../../src/main/power-logs-worker.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { chmodSync, lstatSync, mkdirSync, readdirSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { isAbsolute, join, relative } from \"node:path\";\nimport { type UtilityProcess, utilityProcess } from \"electron\";\nimport { POWER_LOGS_LIMITS as L, type LogRequest } from \"../common/power-logs\";\nimport { normalizePowerLogsLimits } from \"../common/power-logs-limits\";\n\nimport type { PowerLogsQuerySnapshot } from \"./power-logs-query\";\n\ntype ActiveQuery = {\n request: Extract<LogRequest, { operation: \"query\" }>;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n child?: UtilityProcess;\n expiry?: ReturnType<typeof setTimeout>;\n};\n\nexport function sweepPowerLogsStorage(directory = join(tmpdir(), \"freelens-power-logs\")): string {\n mkdirSync(directory, { recursive: true, mode: 0o700 });\n const root = lstatSync(directory);\n if (!root.isDirectory() || root.isSymbolicLink() || (process.getuid && root.uid !== process.getuid()))\n throw new Error(\"Log storage unavailable\");\n chmodSync(directory, 0o700);\n for (const name of readdirSync(directory)) {\n const match = /^([1-9][0-9]*)-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/.exec(name);\n if (!match) continue;\n const pid = Number(match[1]);\n if (!Number.isSafeInteger(pid) || pid > 2147483647) continue;\n const path = join(directory, name);\n try {\n const entry = lstatSync(path);\n if (!entry.isDirectory() || entry.isSymbolicLink() || (process.getuid && entry.uid !== process.getuid()))\n continue;\n try {\n process.kill(pid, 0);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ESRCH\") rmSync(path, { recursive: true, force: true });\n }\n } catch {\n /* another process may have completed cleanup */\n }\n }\n return directory;\n}\n\nexport class PowerLogsWorker {\n private child: UtilityProcess;\n private directory: string;\n private pending = new Map<\n number,\n { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }\n >();\n private sequence = 0;\n private closed = false;\n private children = new Set<UtilityProcess>();\n private query?: ActiveQuery;\n private readers = new Map<UtilityProcess, LogRequest & { captureId: string }>();\n private deleting = new Set<string>();\n private captureKey(request: LogRequest & { captureId: string }): string {\n return JSON.stringify([request.clusterId, request.contextId, request.captureId]);\n }\n get isClosed(): boolean {\n return this.closed;\n }\n constructor() {\n this.directory = join(sweepPowerLogsStorage(), `${process.pid}-${randomUUID()}`);\n mkdirSync(this.directory, { mode: 0o700 });\n try {\n this.child = utilityProcess.fork(join(__dirname, \"power-logs-child.js\"), [this.directory], {\n serviceName: \"Power Logs\",\n stdio: \"ignore\",\n });\n } catch {\n rmSync(this.directory, { recursive: true, force: true });\n throw new Error(\"Log process unavailable\");\n }\n this.track(this.child);\n this.child.on(\"message\", ({ id, result, error }) => {\n const pending = this.pending.get(id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this.pending.delete(id);\n if (error) pending.reject(new Error(\"Log operation failed or exceeded its limit\"));\n else pending.resolve(result);\n });\n this.child.on(\"exit\", () => {\n this.close();\n });\n }\n private track(child: UtilityProcess): void {\n this.children.add(child);\n child.once(\"exit\", () => {\n this.children.delete(child);\n this.cleanup();\n });\n }\n private cleanup(): void {\n if (this.closed && this.children.size === 0) rmSync(this.directory, { recursive: true, force: true });\n }\n call(request: LogRequest, destination?: string): Promise<unknown> {\n if (request.operation === \"query\") return this.runQuery(request);\n const active = this.query;\n if (\n active &&\n request.clusterId === active.request.clusterId &&\n request.contextId === active.request.contextId &&\n ((request.operation === \"cancel\" && request.queryId === active.request.queryId) ||\n (request.operation === \"delete\" && request.captureId === active.request.captureId))\n ) {\n active.reject(new Error(\"Log query canceled\"));\n }\n if (request.operation === \"delete\") {\n const key = this.captureKey(request);\n this.deleting.add(key);\n const readers = [...this.readers].filter(([, owner]) => this.captureKey(owner) === key);\n if (readers.length === 0) return this.callStorage(request, destination).finally(() => this.deleting.delete(key));\n return Promise.all(\n readers.map(\n ([child]) =>\n new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"Log query process is still closing\")), 5000);\n child.once(\"exit\", () => {\n clearTimeout(timer);\n resolve();\n });\n child.kill();\n }),\n ),\n )\n .then(() => this.callStorage(request, destination))\n .finally(() => this.deleting.delete(key));\n }\n return this.callStorage(request, destination);\n }\n private runQuery(request: Extract<LogRequest, { operation: \"query\" }>): Promise<unknown> {\n if (this.closed || this.query || this.deleting.has(this.captureKey(request)))\n return Promise.reject(new Error(\"Log query unavailable or busy\"));\n return new Promise((resolve, reject) => {\n const finish = (error?: Error, result?: unknown) => {\n if (this.query !== active) return;\n this.query = undefined;\n clearTimeout(active.timer);\n clearTimeout(active.expiry);\n active.child?.kill();\n if (error) reject(error);\n else resolve(result);\n };\n const active: ActiveQuery = {\n request,\n reject: (error) => finish(error),\n timer: setTimeout(() => finish(new Error(\"Log query exceeded its time limit\")), L.queryMs),\n };\n this.query = active;\n void this.callStorage(request).then(\n (value) => {\n if (this.query !== active) return;\n const snapshot = value as PowerLogsQuerySnapshot;\n const local = typeof snapshot?.database === \"string\" ? relative(this.directory, snapshot.database) : \"..\";\n if (\n !local ||\n local.startsWith(\"..\") ||\n isAbsolute(local) ||\n snapshot.captureId !== request.captureId ||\n !Number.isFinite(snapshot.expiresAt) ||\n snapshot.expiresAt <= Date.now()\n ) {\n finish(new Error(\"Log snapshot unavailable\"));\n return;\n }\n try {\n const child = utilityProcess.fork(join(__dirname, \"power-logs-query-child.js\"), [], {\n serviceName: \"Power Logs SQL\",\n stdio: \"ignore\",\n });\n active.child = child;\n this.track(child);\n this.readers.set(child, request);\n child.once(\"exit\", () => this.readers.delete(child));\n active.expiry = setTimeout(\n () => finish(new Error(\"Log snapshot expired\")),\n snapshot.expiresAt - Date.now(),\n );\n child.once(\"exit\", () => {\n clearTimeout(active.expiry);\n finish(new Error(\"Log query process unavailable\"));\n });\n child.once(\"message\", ({ id, result, error }) => {\n if (id !== 1) return;\n clearTimeout(active.expiry);\n finish(error ? new Error(\"Log query failed or exceeded its limit\") : undefined, result);\n });\n child.postMessage({\n id: 1,\n database: snapshot.database,\n sql: request.sql,\n limits: normalizePowerLogsLimits(L),\n });\n } catch {\n finish(new Error(\"Log query process unavailable\"));\n }\n },\n (error: Error) => finish(error),\n );\n });\n }\n private callStorage(request: LogRequest, destination?: string): Promise<unknown> {\n if (this.closed || this.pending.size >= 16) return Promise.reject(new Error(\"Log storage unavailable or busy\"));\n return new Promise((resolve, reject) => {\n const id = ++this.sequence;\n const timer = setTimeout(() => this.close(), 20000);\n this.pending.set(id, { resolve, reject, timer });\n try {\n this.child.postMessage({ id, request, destination, limits: normalizePowerLogsLimits(L) });\n } catch {\n this.close();\n }\n });\n }\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.query?.reject(new Error(\"Log storage unavailable\"));\n for (const child of this.children) child.kill();\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(new Error(\"Log storage unavailable\"));\n }\n this.pending.clear();\n this.cleanup();\n }\n}\n"],"mappings":"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"}
@@ -3,10 +3,10 @@ const require__global_external_mobx = require("./_virtual/_global-external_mobx.
3
3
  const require_monitoring_preferences_store = require("./common/store/monitoring-preferences-store.js");
4
4
  const require_resource_search = require("./common/resource-search.js");
5
5
  const require_resource_search_worker = require("./resource-search-worker.js");
6
+ let node_crypto = require("node:crypto");
6
7
  let electron = require("electron");
7
8
  let node_path = require("node:path");
8
9
  let node_fs = require("node:fs");
9
- let node_crypto = require("node:crypto");
10
10
  //#region src/main/resource-search.ts
11
11
  function trustedSearchSender(event, clusterId) {
12
12
  const window = electron.BrowserWindow.fromWebContents(event.sender);
@@ -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: {