fraim 2.0.161 → 2.0.162

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.
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AiHubConversationStore = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
10
+ const emptyProjectState = () => ({
11
+ activeId: null,
12
+ conversations: [],
13
+ });
14
+ function timestampValue(value) {
15
+ if (typeof value === 'number')
16
+ return Number.isFinite(value) ? value : 0;
17
+ if (typeof value === 'string') {
18
+ const parsed = Date.parse(value);
19
+ if (Number.isFinite(parsed))
20
+ return parsed;
21
+ const numeric = Number(value);
22
+ return Number.isFinite(numeric) ? numeric : 0;
23
+ }
24
+ return 0;
25
+ }
26
+ function normalizeProjectPath(projectPath) {
27
+ return path_1.default.resolve(projectPath || process.cwd());
28
+ }
29
+ function normalizeConversation(projectPath, raw) {
30
+ if (!raw || typeof raw !== 'object')
31
+ return null;
32
+ const value = raw;
33
+ if (typeof value.id !== 'string' || value.id.length === 0)
34
+ return null;
35
+ if (typeof value.title !== 'string')
36
+ return null;
37
+ if (typeof value.jobId !== 'string')
38
+ return null;
39
+ if (value.agentName !== 'codex' && value.agentName !== 'claude' && value.agentName !== 'gemini')
40
+ return null;
41
+ if (value.status !== 'running' && value.status !== 'completed' && value.status !== 'failed')
42
+ return null;
43
+ return {
44
+ ...value,
45
+ id: value.id,
46
+ projectPath: normalizeProjectPath(typeof value.projectPath === 'string' ? value.projectPath : projectPath),
47
+ title: value.title,
48
+ jobId: value.jobId,
49
+ agentName: value.agentName,
50
+ status: value.status,
51
+ createdAt: value.createdAt ?? new Date().toISOString(),
52
+ lastUpdatedAt: value.lastUpdatedAt ?? value.createdAt ?? new Date().toISOString(),
53
+ };
54
+ }
55
+ function normalizeProjectState(projectPath, raw) {
56
+ if (!raw || typeof raw !== 'object')
57
+ return emptyProjectState();
58
+ const value = raw;
59
+ const conversations = Array.isArray(value.conversations)
60
+ ? value.conversations.map((entry) => normalizeConversation(projectPath, entry)).filter((entry) => Boolean(entry))
61
+ : [];
62
+ const activeId = typeof value.activeId === 'string' && conversations.some((entry) => entry.id === value.activeId)
63
+ ? value.activeId
64
+ : null;
65
+ return { activeId, conversations };
66
+ }
67
+ class AiHubConversationStore {
68
+ constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-conversations.json')) {
69
+ this.stateFilePath = stateFilePath;
70
+ }
71
+ loadProject(projectPath) {
72
+ const state = this.readStore();
73
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
74
+ return normalizeProjectState(normalizedProjectPath, state.projects[normalizedProjectPath]);
75
+ }
76
+ replaceProject(projectPath, next) {
77
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
78
+ const normalized = normalizeProjectState(normalizedProjectPath, next);
79
+ const state = this.readStore();
80
+ state.projects[normalizedProjectPath] = normalized;
81
+ this.writeStore(state);
82
+ return normalized;
83
+ }
84
+ upsertConversation(projectPath, conversation, activeId) {
85
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
86
+ const normalizedConversation = normalizeConversation(normalizedProjectPath, conversation);
87
+ if (!normalizedConversation) {
88
+ return this.loadProject(normalizedProjectPath);
89
+ }
90
+ const current = this.loadProject(normalizedProjectPath);
91
+ const idx = current.conversations.findIndex((entry) => entry.id === normalizedConversation.id);
92
+ if (idx >= 0) {
93
+ const existing = current.conversations[idx];
94
+ if (timestampValue(normalizedConversation.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
95
+ return current;
96
+ }
97
+ current.conversations[idx] = normalizedConversation;
98
+ }
99
+ else {
100
+ current.conversations.unshift(normalizedConversation);
101
+ }
102
+ current.activeId = activeId === undefined ? current.activeId : activeId;
103
+ if (current.activeId && !current.conversations.some((entry) => entry.id === current.activeId)) {
104
+ current.activeId = null;
105
+ }
106
+ return this.replaceProject(normalizedProjectPath, current);
107
+ }
108
+ patchConversation(projectPath, conversationId, patch) {
109
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
110
+ const current = this.loadProject(normalizedProjectPath);
111
+ const idx = current.conversations.findIndex((entry) => entry.id === conversationId);
112
+ if (idx < 0) {
113
+ const candidate = normalizeConversation(normalizedProjectPath, {
114
+ ...patch,
115
+ id: conversationId,
116
+ projectPath: normalizedProjectPath,
117
+ });
118
+ if (candidate) {
119
+ current.conversations.unshift(candidate);
120
+ return this.replaceProject(normalizedProjectPath, current);
121
+ }
122
+ return current;
123
+ }
124
+ const existing = current.conversations[idx];
125
+ if (patch.lastUpdatedAt !== undefined
126
+ && timestampValue(patch.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
127
+ return current;
128
+ }
129
+ current.conversations[idx] = {
130
+ ...existing,
131
+ ...patch,
132
+ id: existing.id,
133
+ projectPath: normalizedProjectPath,
134
+ agentName: patch.agentName || existing.agentName,
135
+ lastUpdatedAt: patch.lastUpdatedAt ?? new Date().toISOString(),
136
+ };
137
+ return this.replaceProject(normalizedProjectPath, current);
138
+ }
139
+ readStore() {
140
+ if (!fs_1.default.existsSync(this.stateFilePath)) {
141
+ return { version: 1, projects: {} };
142
+ }
143
+ try {
144
+ const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
145
+ if (raw.version !== 1 || !raw.projects || typeof raw.projects !== 'object') {
146
+ return { version: 1, projects: {} };
147
+ }
148
+ return {
149
+ version: 1,
150
+ projects: raw.projects,
151
+ };
152
+ }
153
+ catch {
154
+ return { version: 1, projects: {} };
155
+ }
156
+ }
157
+ writeStore(store) {
158
+ fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
159
+ const tempPath = `${this.stateFilePath}.${process.pid}.tmp`;
160
+ fs_1.default.writeFileSync(tempPath, JSON.stringify(store, null, 2), 'utf8');
161
+ fs_1.default.renameSync(tempPath, this.stateFilePath);
162
+ }
163
+ }
164
+ exports.AiHubConversationStore = AiHubConversationStore;
@@ -174,7 +174,12 @@ async function createWindow(url) {
174
174
  backgroundColor: (isMac || isWin) ? '#00000000' : '#ECECEC',
175
175
  titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
176
176
  ...(isWin && {
177
- titleBarOverlay: { color: 'rgba(0,0,0,0)', symbolColor: '#1A1A1A', height: 36 },
177
+ titleBarOverlay: {
178
+ color: 'rgba(0,0,0,0)',
179
+ // Glyphs must contrast the Mica material: light in dark mode, dark in light mode.
180
+ symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
181
+ height: 36,
182
+ },
178
183
  }),
179
184
  ...(isWin && { backgroundMaterial: 'mica' }),
180
185
  ...(isMac && { vibrancy: 'sidebar' }),
@@ -182,6 +187,24 @@ async function createWindow(url) {
182
187
  webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
183
188
  });
184
189
  electron_1.Menu.setApplicationMenu(null);
190
+ // Keep the Windows titlebar control glyphs readable when the OS theme flips
191
+ // between light and dark while the window is open.
192
+ if (isWin) {
193
+ const applyOverlay = () => {
194
+ if (!mainWindow)
195
+ return;
196
+ try {
197
+ mainWindow.setTitleBarOverlay({
198
+ color: 'rgba(0,0,0,0)',
199
+ symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
200
+ height: 36,
201
+ });
202
+ }
203
+ catch { /* overlay unsupported on this Windows build */ }
204
+ };
205
+ electron_1.nativeTheme.on('updated', applyOverlay);
206
+ mainWindow.on('closed', () => electron_1.nativeTheme.removeListener('updated', applyOverlay));
207
+ }
185
208
  mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
186
209
  void electron_1.shell.openExternal(targetUrl);
187
210
  return { action: 'deny' };