@adhdev/daemon-core 0.7.45 → 0.7.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -0
- package/dist/cli-adapters/pty-transport.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +11 -2
- package/dist/config/chat-history.d.ts +29 -2
- package/dist/config/config.d.ts +4 -0
- package/dist/config/recent-activity.d.ts +3 -1
- package/dist/config/saved-sessions.d.ts +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4619 -3969
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4613 -3965
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +24 -1
- package/dist/providers/contracts.d.ts +3 -0
- package/dist/providers/provider-instance.d.ts +1 -0
- package/dist/shared-types.d.ts +2 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +19 -15
- package/src/cli-adapters/provider-cli-adapter.ts +7 -1
- package/src/cli-adapters/pty-transport.ts +1 -0
- package/src/cli-adapters/session-host-transport.ts +19 -0
- package/src/commands/chat-commands.ts +28 -8
- package/src/commands/cli-manager.ts +259 -22
- package/src/commands/router.ts +52 -1
- package/src/config/chat-history.ts +193 -10
- package/src/config/config.d.ts +4 -0
- package/src/config/config.ts +6 -0
- package/src/config/recent-activity.ts +13 -2
- package/src/config/saved-sessions.ts +73 -0
- package/src/daemon/dev-auto-implement.ts +23 -5
- package/src/daemon/dev-server.ts +22 -4
- package/src/index.ts +2 -0
- package/src/providers/cli-provider-instance.ts +205 -4
- package/src/providers/contracts.ts +3 -0
- package/src/providers/provider-instance.d.ts +1 -0
- package/src/providers/provider-instance.ts +1 -0
- package/src/session-host/runtime-support.ts +1 -0
- package/src/shared-types.d.ts +2 -0
- package/src/shared-types.ts +2 -0
- package/src/status/builders.ts +1 -0
- package/src/status/snapshot.ts +1 -0
|
@@ -21,11 +21,22 @@ interface HistoryMessage {
|
|
|
21
21
|
receivedAt: number; // epoch ms
|
|
22
22
|
role: 'user' | 'assistant' | 'system';
|
|
23
23
|
content: string;
|
|
24
|
+
kind?: string;
|
|
24
25
|
agent: string; // e.g. 'antigravity', 'cursor', 'gemini-cli'
|
|
25
26
|
instanceId?: string; // IDE instance UUID (distinguishes windows of the same agent type)
|
|
27
|
+
historySessionId?: string; // Persistent provider-side conversation/session key
|
|
26
28
|
sessionTitle?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export interface SavedHistorySessionSummary {
|
|
32
|
+
historySessionId: string;
|
|
33
|
+
sessionTitle?: string;
|
|
34
|
+
messageCount: number;
|
|
35
|
+
firstMessageAt: number;
|
|
36
|
+
lastMessageAt: number;
|
|
37
|
+
preview?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
export class ChatHistoryWriter {
|
|
30
41
|
/** Last seen message count per agent (deduplication) */
|
|
31
42
|
private lastSeenCounts = new Map<string, number>();
|
|
@@ -43,15 +54,17 @@ export class ChatHistoryWriter {
|
|
|
43
54
|
*/
|
|
44
55
|
appendNewMessages(
|
|
45
56
|
agentType: string,
|
|
46
|
-
messages: Array<{ role: string; content: string; receivedAt?: number }>,
|
|
57
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; historyDedupKey?: string }>,
|
|
47
58
|
sessionTitle?: string,
|
|
48
59
|
instanceId?: string,
|
|
60
|
+
historySessionId?: string,
|
|
49
61
|
): void {
|
|
50
62
|
if (!messages || messages.length === 0) return;
|
|
51
63
|
|
|
52
64
|
try {
|
|
53
|
-
// dedup key: agentType + instanceId
|
|
54
|
-
const
|
|
65
|
+
// dedup key: agentType + persistent history key (fallback: runtime instanceId)
|
|
66
|
+
const effectiveHistoryKey = historySessionId || instanceId;
|
|
67
|
+
const dedupKey = effectiveHistoryKey ? `${agentType}:${effectiveHistoryKey}` : agentType;
|
|
55
68
|
let seenHashes = this.lastSeenHashes.get(dedupKey);
|
|
56
69
|
if (!seenHashes) {
|
|
57
70
|
seenHashes = new Set<string>();
|
|
@@ -61,7 +74,7 @@ export class ChatHistoryWriter {
|
|
|
61
74
|
// Filter new messages
|
|
62
75
|
const newMessages: HistoryMessage[] = [];
|
|
63
76
|
for (const msg of messages) {
|
|
64
|
-
const hash = `${msg.role}:${(msg.content || '').slice(0, 50)}`;
|
|
77
|
+
const hash = msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`;
|
|
65
78
|
if (seenHashes.has(hash)) continue;
|
|
66
79
|
seenHashes.add(hash);
|
|
67
80
|
newMessages.push({
|
|
@@ -69,20 +82,22 @@ export class ChatHistoryWriter {
|
|
|
69
82
|
receivedAt: msg.receivedAt || Date.now(),
|
|
70
83
|
role: msg.role as 'user' | 'assistant' | 'system',
|
|
71
84
|
content: msg.content || '',
|
|
85
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
72
86
|
agent: agentType,
|
|
73
87
|
instanceId,
|
|
88
|
+
historySessionId: effectiveHistoryKey,
|
|
74
89
|
sessionTitle,
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
if (newMessages.length === 0) return;
|
|
79
94
|
|
|
80
|
-
// Append to file —
|
|
95
|
+
// Append to file — keyed by persistent history session when available
|
|
81
96
|
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
82
97
|
fs.mkdirSync(dir, { recursive: true });
|
|
83
98
|
|
|
84
99
|
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
85
|
-
const filePrefix =
|
|
100
|
+
const filePrefix = effectiveHistoryKey ? `${this.sanitize(effectiveHistoryKey)}_` : '';
|
|
86
101
|
const filePath = path.join(dir, `${filePrefix}${date}.jsonl`);
|
|
87
102
|
const lines = newMessages.map(m => JSON.stringify(m)).join('\n') + '\n';
|
|
88
103
|
fs.appendFileSync(filePath, lines, 'utf-8');
|
|
@@ -92,7 +107,7 @@ export class ChatHistoryWriter {
|
|
|
92
107
|
if (messages.length < prevCount * 0.5 && prevCount > 3) {
|
|
93
108
|
seenHashes.clear();
|
|
94
109
|
for (const msg of messages) {
|
|
95
|
-
seenHashes.add(`${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
110
|
+
seenHashes.add(msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
this.lastSeenCounts.set(dedupKey, messages.length);
|
|
@@ -107,6 +122,101 @@ export class ChatHistoryWriter {
|
|
|
107
122
|
}
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
appendSystemMarker(
|
|
126
|
+
agentType: string,
|
|
127
|
+
content: string,
|
|
128
|
+
options: {
|
|
129
|
+
sessionTitle?: string;
|
|
130
|
+
instanceId?: string;
|
|
131
|
+
historySessionId?: string;
|
|
132
|
+
dedupKey?: string;
|
|
133
|
+
receivedAt?: number;
|
|
134
|
+
} = {},
|
|
135
|
+
): void {
|
|
136
|
+
this.appendNewMessages(
|
|
137
|
+
agentType,
|
|
138
|
+
[{
|
|
139
|
+
role: 'system',
|
|
140
|
+
kind: 'system',
|
|
141
|
+
content,
|
|
142
|
+
receivedAt: options.receivedAt,
|
|
143
|
+
historyDedupKey: options.dedupKey,
|
|
144
|
+
}],
|
|
145
|
+
options.sessionTitle,
|
|
146
|
+
options.instanceId,
|
|
147
|
+
options.historySessionId,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
promoteHistorySession(
|
|
152
|
+
agentType: string,
|
|
153
|
+
previousHistorySessionId: string,
|
|
154
|
+
nextHistorySessionId: string,
|
|
155
|
+
): void {
|
|
156
|
+
const fromId = String(previousHistorySessionId || '').trim();
|
|
157
|
+
const toId = String(nextHistorySessionId || '').trim();
|
|
158
|
+
if (!fromId || !toId || fromId === toId) return;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const fromDedupKey = `${agentType}:${fromId}`;
|
|
162
|
+
const toDedupKey = `${agentType}:${toId}`;
|
|
163
|
+
const fromHashes = this.lastSeenHashes.get(fromDedupKey);
|
|
164
|
+
if (fromHashes?.size) {
|
|
165
|
+
const nextHashes = this.lastSeenHashes.get(toDedupKey) || new Set<string>();
|
|
166
|
+
for (const hash of fromHashes) nextHashes.add(hash);
|
|
167
|
+
this.lastSeenHashes.set(toDedupKey, nextHashes);
|
|
168
|
+
this.lastSeenHashes.delete(fromDedupKey);
|
|
169
|
+
}
|
|
170
|
+
const fromCount = this.lastSeenCounts.get(fromDedupKey);
|
|
171
|
+
if (typeof fromCount === 'number') {
|
|
172
|
+
this.lastSeenCounts.set(toDedupKey, Math.max(fromCount, this.lastSeenCounts.get(toDedupKey) || 0));
|
|
173
|
+
this.lastSeenCounts.delete(fromDedupKey);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
177
|
+
if (!fs.existsSync(dir)) return;
|
|
178
|
+
|
|
179
|
+
const fromPrefix = `${this.sanitize(fromId)}_`;
|
|
180
|
+
const toPrefix = `${this.sanitize(toId)}_`;
|
|
181
|
+
const files = fs.readdirSync(dir).filter((file) => file.startsWith(fromPrefix) && file.endsWith('.jsonl'));
|
|
182
|
+
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
const sourcePath = path.join(dir, file);
|
|
185
|
+
const targetPath = path.join(dir, `${toPrefix}${file.slice(fromPrefix.length)}`);
|
|
186
|
+
const sourceLines = fs.readFileSync(sourcePath, 'utf-8').split('\n').filter(Boolean);
|
|
187
|
+
const rewritten = sourceLines
|
|
188
|
+
.map((line) => {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(line) as HistoryMessage;
|
|
191
|
+
if (parsed.historySessionId !== fromId) return null;
|
|
192
|
+
return JSON.stringify({
|
|
193
|
+
...parsed,
|
|
194
|
+
historySessionId: toId,
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
.filter((line): line is string => !!line);
|
|
201
|
+
if (rewritten.length === 0) {
|
|
202
|
+
fs.unlinkSync(sourcePath);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const existing = fs.existsSync(targetPath)
|
|
207
|
+
? new Set(fs.readFileSync(targetPath, 'utf-8').split('\n').filter(Boolean))
|
|
208
|
+
: new Set<string>();
|
|
209
|
+
const nextLines = rewritten.filter((line) => !existing.has(line));
|
|
210
|
+
if (nextLines.length > 0) {
|
|
211
|
+
fs.appendFileSync(targetPath, `${nextLines.join('\n')}\n`, 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
fs.unlinkSync(sourcePath);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Ignore promotion failure; future messages will still write to the new session key.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
110
220
|
/** Called when agent session is explicitly changed */
|
|
111
221
|
onSessionChange(agentType: string): void {
|
|
112
222
|
this.lastSeenHashes.delete(agentType);
|
|
@@ -157,15 +267,15 @@ export function readChatHistory(
|
|
|
157
267
|
agentType: string,
|
|
158
268
|
offset: number = 0,
|
|
159
269
|
limit: number = 30,
|
|
160
|
-
|
|
270
|
+
historySessionId?: string,
|
|
161
271
|
): { messages: HistoryMessage[]; hasMore: boolean } {
|
|
162
272
|
try {
|
|
163
273
|
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
164
274
|
const dir = path.join(HISTORY_DIR, sanitized);
|
|
165
275
|
if (!fs.existsSync(dir)) return { messages: [], hasMore: false };
|
|
166
276
|
|
|
167
|
-
// JSONL file list — filter by
|
|
168
|
-
const sanitizedInstance =
|
|
277
|
+
// JSONL file list — filter by persistent history key when specified
|
|
278
|
+
const sanitizedInstance = historySessionId?.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
169
279
|
const files = fs.readdirSync(dir)
|
|
170
280
|
.filter(f => {
|
|
171
281
|
if (!f.endsWith('.jsonl')) return false;
|
|
@@ -210,3 +320,76 @@ export function readChatHistory(
|
|
|
210
320
|
return { messages: [], hasMore: false };
|
|
211
321
|
}
|
|
212
322
|
}
|
|
323
|
+
|
|
324
|
+
export function listSavedHistorySessions(
|
|
325
|
+
agentType: string,
|
|
326
|
+
options: { offset?: number; limit?: number } = {},
|
|
327
|
+
): { sessions: SavedHistorySessionSummary[]; hasMore: boolean } {
|
|
328
|
+
try {
|
|
329
|
+
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
330
|
+
const dir = path.join(HISTORY_DIR, sanitized);
|
|
331
|
+
if (!fs.existsSync(dir)) return { sessions: [], hasMore: false };
|
|
332
|
+
|
|
333
|
+
const groupedFiles = new Map<string, string[]>();
|
|
334
|
+
const filePattern = /^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
335
|
+
for (const file of fs.readdirSync(dir)) {
|
|
336
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
337
|
+
const match = file.match(filePattern);
|
|
338
|
+
if (!match?.[1]) continue;
|
|
339
|
+
const historySessionId = match[1];
|
|
340
|
+
const files = groupedFiles.get(historySessionId) || [];
|
|
341
|
+
files.push(file);
|
|
342
|
+
groupedFiles.set(historySessionId, files);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const summaries: SavedHistorySessionSummary[] = [];
|
|
346
|
+
for (const [historySessionId, files] of groupedFiles.entries()) {
|
|
347
|
+
let messageCount = 0;
|
|
348
|
+
let firstMessageAt = 0;
|
|
349
|
+
let lastMessageAt = 0;
|
|
350
|
+
let sessionTitle = '';
|
|
351
|
+
let preview = '';
|
|
352
|
+
|
|
353
|
+
for (const file of files.sort()) {
|
|
354
|
+
const filePath = path.join(dir, file);
|
|
355
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
356
|
+
const lines = content.split('\n').filter(Boolean);
|
|
357
|
+
for (const line of lines) {
|
|
358
|
+
let parsed: HistoryMessage | null = null;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
361
|
+
} catch {
|
|
362
|
+
parsed = null;
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || parsed.historySessionId !== historySessionId) continue;
|
|
365
|
+
messageCount += 1;
|
|
366
|
+
if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
|
|
367
|
+
if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
|
|
368
|
+
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
369
|
+
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (messageCount === 0 || !lastMessageAt) continue;
|
|
374
|
+
summaries.push({
|
|
375
|
+
historySessionId,
|
|
376
|
+
sessionTitle: sessionTitle || undefined,
|
|
377
|
+
messageCount,
|
|
378
|
+
firstMessageAt,
|
|
379
|
+
lastMessageAt,
|
|
380
|
+
preview: preview || undefined,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
385
|
+
const offset = Math.max(0, options.offset || 0);
|
|
386
|
+
const limit = Math.max(1, options.limit || 30);
|
|
387
|
+
const sliced = summaries.slice(offset, offset + limit);
|
|
388
|
+
return {
|
|
389
|
+
sessions: sliced,
|
|
390
|
+
hasMore: summaries.length > offset + limit,
|
|
391
|
+
};
|
|
392
|
+
} catch {
|
|
393
|
+
return { sessions: [], hasMore: false };
|
|
394
|
+
}
|
|
395
|
+
}
|
package/src/config/config.d.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
7
7
|
import type { RecentActivityEntry } from './recent-activity.js';
|
|
8
|
+
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
8
9
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
9
10
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
11
|
+
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
10
12
|
export interface ADHDevConfig {
|
|
11
13
|
serverUrl: string;
|
|
12
14
|
selectedIde: string | null;
|
|
@@ -23,6 +25,8 @@ export interface ADHDevConfig {
|
|
|
23
25
|
defaultWorkspaceId?: string | null;
|
|
24
26
|
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
25
27
|
recentActivity?: RecentActivityEntry[];
|
|
28
|
+
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
29
|
+
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
26
30
|
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
27
31
|
sessionReads?: Record<string, number>;
|
|
28
32
|
/** Last seen completion marker for live sessions, keyed by sessionId */
|
package/src/config/config.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'f
|
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
11
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
12
12
|
import type { RecentActivityEntry } from './recent-activity.js';
|
|
13
|
+
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
13
14
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
14
15
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
16
|
+
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
15
17
|
|
|
16
18
|
export interface ADHDevConfig {
|
|
17
19
|
// Server connection
|
|
@@ -44,6 +46,8 @@ export interface ADHDevConfig {
|
|
|
44
46
|
|
|
45
47
|
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
46
48
|
recentActivity?: RecentActivityEntry[];
|
|
49
|
+
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
50
|
+
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
47
51
|
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
48
52
|
sessionReads?: Record<string, number>;
|
|
49
53
|
/** Last seen completion marker for live sessions, keyed by sessionId */
|
|
@@ -99,6 +103,7 @@ const DEFAULT_CONFIG: ADHDevConfig = {
|
|
|
99
103
|
workspaces: [],
|
|
100
104
|
defaultWorkspaceId: null,
|
|
101
105
|
recentActivity: [],
|
|
106
|
+
savedProviderSessions: [],
|
|
102
107
|
sessionReads: {},
|
|
103
108
|
sessionReadMarkers: {},
|
|
104
109
|
machineNickname: null,
|
|
@@ -161,6 +166,7 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
|
|
|
161
166
|
workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
|
|
162
167
|
defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
|
|
163
168
|
recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
|
|
169
|
+
savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
|
|
164
170
|
sessionReads: mergedSessionReads,
|
|
165
171
|
sessionReadMarkers,
|
|
166
172
|
machineNickname: asNullableString(parsed.machineNickname),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Unlike live session state, this is launch oriented:
|
|
5
5
|
* - one normalized row shape for IDE / CLI / ACP
|
|
6
|
-
* - deduped by kind + providerType + workspace
|
|
6
|
+
* - deduped by provider session when available, else by kind + providerType + workspace
|
|
7
7
|
* - used only for quick-launch shortcuts
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ export interface RecentActivityEntry {
|
|
|
16
16
|
kind: 'ide' | 'cli' | 'acp';
|
|
17
17
|
providerType: string;
|
|
18
18
|
providerName: string;
|
|
19
|
+
providerSessionId?: string;
|
|
19
20
|
workspace?: string | null;
|
|
20
21
|
currentModel?: string;
|
|
21
22
|
title?: string;
|
|
@@ -37,6 +38,16 @@ export function buildRecentActivityKey(entry: Pick<RecentActivityEntry, 'kind' |
|
|
|
37
38
|
return `${entry.kind}:${entry.providerType}:${normalizeWorkspace(entry.workspace)}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export function buildRecentActivityKeyForEntry(
|
|
42
|
+
entry: Pick<RecentActivityEntry, 'kind' | 'providerType' | 'workspace' | 'providerSessionId'>,
|
|
43
|
+
) {
|
|
44
|
+
const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
|
|
45
|
+
if (providerSessionId) {
|
|
46
|
+
return `${entry.kind}:${entry.providerType}:session:${providerSessionId}`;
|
|
47
|
+
}
|
|
48
|
+
return buildRecentActivityKey(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export function appendRecentActivity(
|
|
41
52
|
config: ADHDevConfig,
|
|
42
53
|
entry: Omit<RecentActivityEntry, 'id' | 'lastUsedAt'> & { lastUsedAt?: number },
|
|
@@ -44,7 +55,7 @@ export function appendRecentActivity(
|
|
|
44
55
|
const nextEntry: RecentActivityEntry = {
|
|
45
56
|
...entry,
|
|
46
57
|
workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
|
|
47
|
-
id:
|
|
58
|
+
id: buildRecentActivityKeyForEntry(entry),
|
|
48
59
|
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
49
60
|
};
|
|
50
61
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import type { ADHDevConfig } from './config.js';
|
|
3
|
+
import { expandPath } from './workspaces.js';
|
|
4
|
+
|
|
5
|
+
export interface SavedProviderSessionEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: 'cli' | 'acp';
|
|
8
|
+
providerType: string;
|
|
9
|
+
providerName: string;
|
|
10
|
+
providerSessionId: string;
|
|
11
|
+
workspace?: string | null;
|
|
12
|
+
currentModel?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
lastUsedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_SAVED_SESSIONS = 500;
|
|
19
|
+
|
|
20
|
+
function normalizeWorkspace(workspace?: string | null) {
|
|
21
|
+
if (!workspace) return '';
|
|
22
|
+
try {
|
|
23
|
+
return path.resolve(expandPath(workspace));
|
|
24
|
+
} catch {
|
|
25
|
+
return path.resolve(workspace);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSavedProviderSessionKey(providerSessionId: string) {
|
|
30
|
+
return `saved:${providerSessionId.trim()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function upsertSavedProviderSession(
|
|
34
|
+
config: ADHDevConfig,
|
|
35
|
+
entry: Omit<SavedProviderSessionEntry, 'id' | 'createdAt' | 'lastUsedAt'> & { createdAt?: number; lastUsedAt?: number },
|
|
36
|
+
): ADHDevConfig {
|
|
37
|
+
const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
|
|
38
|
+
if (!providerSessionId) return config;
|
|
39
|
+
|
|
40
|
+
const id = buildSavedProviderSessionKey(providerSessionId);
|
|
41
|
+
const existing = (config.savedProviderSessions || []).find(item => item.id === id);
|
|
42
|
+
const nextEntry: SavedProviderSessionEntry = {
|
|
43
|
+
id,
|
|
44
|
+
kind: entry.kind,
|
|
45
|
+
providerType: entry.providerType,
|
|
46
|
+
providerName: entry.providerName,
|
|
47
|
+
providerSessionId,
|
|
48
|
+
workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
|
|
49
|
+
currentModel: entry.currentModel,
|
|
50
|
+
title: entry.title,
|
|
51
|
+
createdAt: existing?.createdAt || entry.createdAt || Date.now(),
|
|
52
|
+
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const filtered = (config.savedProviderSessions || []).filter(item => item.id !== id);
|
|
56
|
+
return {
|
|
57
|
+
...config,
|
|
58
|
+
savedProviderSessions: [nextEntry, ...filtered].slice(0, MAX_SAVED_SESSIONS),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getSavedProviderSessions(
|
|
63
|
+
config: ADHDevConfig,
|
|
64
|
+
filters?: { providerType?: string; kind?: SavedProviderSessionEntry['kind'] },
|
|
65
|
+
): SavedProviderSessionEntry[] {
|
|
66
|
+
return [...(config.savedProviderSessions || [])]
|
|
67
|
+
.filter(entry => {
|
|
68
|
+
if (filters?.providerType && entry.providerType !== filters.providerType) return false;
|
|
69
|
+
if (filters?.kind && entry.kind !== filters.kind) return false;
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
72
|
+
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
73
|
+
}
|
|
@@ -952,22 +952,32 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
|
|
|
952
952
|
lines.push('| `detectStatus` | `{ tail, screenText, rawBuffer }` | `idle`, `generating`, `waiting_approval`, or `error` |');
|
|
953
953
|
lines.push('| `parseApproval` | `{ buffer, rawBuffer, tail }` | `{ message, buttons }` or `null` |');
|
|
954
954
|
lines.push('');
|
|
955
|
+
lines.push('## Primary Source of Truth');
|
|
956
|
+
lines.push('The runtime now provides a reliable current-screen snapshot. Treat `screenText` as the primary source of truth for the LIVE visible UI.');
|
|
957
|
+
lines.push('That means:');
|
|
958
|
+
lines.push('- Use `screenText` first for prompt detection, approval UI, status, and visible assistant content.');
|
|
959
|
+
lines.push('- Use `rawBuffer` only as supporting evidence when ANSI/style/cursor cues matter.');
|
|
960
|
+
lines.push('- Use `buffer` only when the visible screen does not contain enough text to recover the latest assistant answer.');
|
|
961
|
+
lines.push('- Do NOT build the parser around stale transcript noise if the current screen already gives the answer.');
|
|
962
|
+
lines.push('');
|
|
955
963
|
|
|
956
964
|
lines.push('## Rules');
|
|
957
965
|
lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
|
|
958
966
|
lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
|
|
959
|
-
lines.push('2. Prefer `screenText` for current visible UI state.
|
|
967
|
+
lines.push('2. Prefer `screenText` for current visible UI state. It is now the PTY equivalent of a trustworthy live DOM snapshot.');
|
|
960
968
|
lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
|
|
961
969
|
lines.push('4. Use `partialResponse` for the actively streaming assistant text when status is `generating`.');
|
|
962
|
-
lines.push('5. `detectStatus` must stay lightweight and
|
|
963
|
-
lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
|
|
964
|
-
lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
|
|
970
|
+
lines.push('5. `detectStatus` must stay lightweight and current-screen-oriented. Prefer the active bottom-of-screen region over stale history.');
|
|
971
|
+
lines.push('6. `parseApproval` should understand the live approval area and return clean button labels from the CURRENT visible modal.');
|
|
972
|
+
lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts or style cues matter. Do not depend on raw escape noise unless necessary.');
|
|
965
973
|
lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
|
|
966
974
|
lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions — no "tiny supporting changes" to other files.');
|
|
967
975
|
lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
|
|
968
976
|
lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
|
|
969
977
|
lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
|
|
970
978
|
lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
|
|
979
|
+
lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
|
|
980
|
+
lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
|
|
971
981
|
lines.push('');
|
|
972
982
|
|
|
973
983
|
lines.push('## Task');
|
|
@@ -990,6 +1000,12 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
|
|
|
990
1000
|
lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
|
|
991
1001
|
lines.push('```');
|
|
992
1002
|
lines.push('');
|
|
1003
|
+
lines.push('The debug payload should be read in this priority order:');
|
|
1004
|
+
lines.push('1. `screenText` / current visible state');
|
|
1005
|
+
lines.push('2. parsed `status`, `messages`, `activeModal`');
|
|
1006
|
+
lines.push('3. `rawBuffer` only for style/control-sequence cues');
|
|
1007
|
+
lines.push('4. `buffer` only when the current screen is insufficient');
|
|
1008
|
+
lines.push('');
|
|
993
1009
|
lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
|
|
994
1010
|
lines.push('');
|
|
995
1011
|
lines.push('### 3. Send a realistic approval-triggering prompt');
|
|
@@ -1036,6 +1052,8 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
|
|
|
1036
1052
|
lines.push('5. Confirm the Python file was actually created and executed, not just described in chat text.');
|
|
1037
1053
|
lines.push('6. Confirm the final assistant transcript includes the exact Python output, including the working directory line and the five square numbers.');
|
|
1038
1054
|
lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
|
|
1055
|
+
lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
|
|
1056
|
+
lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
|
|
1039
1057
|
lines.push('');
|
|
1040
1058
|
|
|
1041
1059
|
if (userComment) {
|
|
@@ -1091,4 +1109,4 @@ export function sendAutoImplSSE(ctx: DevServerContext, msg: { event: string; dat
|
|
|
1091
1109
|
for (const client of ctx.autoImplSSEClients) {
|
|
1092
1110
|
try { client.write(payload); } catch { /* ignore */ }
|
|
1093
1111
|
}
|
|
1094
|
-
}
|
|
1112
|
+
}
|
package/src/daemon/dev-server.ts
CHANGED
|
@@ -1427,22 +1427,32 @@ export class DevServer implements DevServerContext {
|
|
|
1427
1427
|
lines.push('| `detectStatus` | `{ tail, screenText, rawBuffer }` | `idle`, `generating`, `waiting_approval`, or `error` |');
|
|
1428
1428
|
lines.push('| `parseApproval` | `{ buffer, rawBuffer, tail }` | `{ message, buttons }` or `null` |');
|
|
1429
1429
|
lines.push('');
|
|
1430
|
+
lines.push('## Primary Source of Truth');
|
|
1431
|
+
lines.push('The runtime now provides a reliable current-screen snapshot. Treat `screenText` as the primary source of truth for the LIVE visible UI.');
|
|
1432
|
+
lines.push('That means:');
|
|
1433
|
+
lines.push('- Use `screenText` first for prompt detection, approval UI, status, and visible assistant content.');
|
|
1434
|
+
lines.push('- Use `rawBuffer` only as supporting evidence when ANSI/style/cursor cues matter.');
|
|
1435
|
+
lines.push('- Use `buffer` only when the visible screen does not contain enough text to recover the latest assistant answer.');
|
|
1436
|
+
lines.push('- Do NOT build the parser around stale transcript noise if the current screen already gives the answer.');
|
|
1437
|
+
lines.push('');
|
|
1430
1438
|
|
|
1431
1439
|
lines.push('## Rules');
|
|
1432
1440
|
lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
|
|
1433
1441
|
lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
|
|
1434
|
-
lines.push('2. Prefer `screenText` for current visible UI state.
|
|
1442
|
+
lines.push('2. Prefer `screenText` for current visible UI state. It is now the PTY equivalent of a trustworthy live DOM snapshot.');
|
|
1435
1443
|
lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
|
|
1436
1444
|
lines.push('4. Use `partialResponse` for the actively streaming assistant text when status is `generating`.');
|
|
1437
|
-
lines.push('5. `detectStatus` must stay lightweight and
|
|
1438
|
-
lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
|
|
1439
|
-
lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
|
|
1445
|
+
lines.push('5. `detectStatus` must stay lightweight and current-screen-oriented. Prefer the active bottom-of-screen region over stale history.');
|
|
1446
|
+
lines.push('6. `parseApproval` should understand the live approval area and return clean button labels from the CURRENT visible modal.');
|
|
1447
|
+
lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts or style cues matter. Do not depend on raw escape noise unless necessary.');
|
|
1440
1448
|
lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
|
|
1441
1449
|
lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions — no "tiny supporting changes" to other files.');
|
|
1442
1450
|
lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
|
|
1443
1451
|
lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
|
|
1444
1452
|
lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
|
|
1445
1453
|
lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
|
|
1454
|
+
lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
|
|
1455
|
+
lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
|
|
1446
1456
|
lines.push('');
|
|
1447
1457
|
|
|
1448
1458
|
lines.push('## Task');
|
|
@@ -1465,6 +1475,12 @@ export class DevServer implements DevServerContext {
|
|
|
1465
1475
|
lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
|
|
1466
1476
|
lines.push('```');
|
|
1467
1477
|
lines.push('');
|
|
1478
|
+
lines.push('The debug payload should be read in this priority order:');
|
|
1479
|
+
lines.push('1. `screenText` / current visible state');
|
|
1480
|
+
lines.push('2. parsed `status`, `messages`, `activeModal`');
|
|
1481
|
+
lines.push('3. `rawBuffer` only for style/control-sequence cues');
|
|
1482
|
+
lines.push('4. `buffer` only when the current screen is insufficient');
|
|
1483
|
+
lines.push('');
|
|
1468
1484
|
lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
|
|
1469
1485
|
lines.push('');
|
|
1470
1486
|
lines.push('### 3. Send a realistic approval-triggering prompt');
|
|
@@ -1511,6 +1527,8 @@ export class DevServer implements DevServerContext {
|
|
|
1511
1527
|
lines.push('5. Confirm the Python file was actually created and executed, not just described in chat text.');
|
|
1512
1528
|
lines.push('6. Confirm the final assistant transcript includes the exact Python output, including the working directory line and the five square numbers.');
|
|
1513
1529
|
lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
|
|
1530
|
+
lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
|
|
1531
|
+
lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
|
|
1514
1532
|
lines.push('');
|
|
1515
1533
|
|
|
1516
1534
|
if (userComment) {
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,8 @@ export { loadConfig, saveConfig, resetConfig, isSetupComplete, markSetupComplete
|
|
|
66
66
|
export { getWorkspaceState } from './config/workspaces.js';
|
|
67
67
|
export { appendRecentActivity, getRecentActivity } from './config/recent-activity.js';
|
|
68
68
|
export type { RecentActivityEntry } from './config/recent-activity.js';
|
|
69
|
+
export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
|
|
70
|
+
export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
|
|
69
71
|
|
|
70
72
|
// ── Detection ──
|
|
71
73
|
export { detectIDEs } from './detection/ide-detector.js';
|