micode 0.8.3 → 0.8.5
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/README.md +2 -2
- package/dist/index.js +21020 -0
- package/package.json +10 -6
- package/src/agents/artifact-searcher.ts +0 -1
- package/src/agents/bootstrapper.ts +164 -0
- package/src/agents/brainstormer.ts +140 -33
- package/src/agents/codebase-analyzer.ts +0 -1
- package/src/agents/codebase-locator.ts +0 -1
- package/src/agents/commander.ts +99 -10
- package/src/agents/executor.ts +18 -1
- package/src/agents/implementer.ts +83 -6
- package/src/agents/index.ts +29 -19
- package/src/agents/ledger-creator.ts +0 -1
- package/src/agents/octto.ts +132 -0
- package/src/agents/pattern-finder.ts +0 -1
- package/src/agents/planner.ts +139 -49
- package/src/agents/probe.ts +152 -0
- package/src/agents/project-initializer.ts +0 -1
- package/src/agents/reviewer.ts +75 -5
- package/src/config-loader.test.ts +226 -0
- package/src/config-loader.ts +132 -6
- package/src/hooks/artifact-auto-index.ts +2 -1
- package/src/hooks/auto-compact.ts +14 -21
- package/src/hooks/context-injector.ts +6 -13
- package/src/hooks/context-window-monitor.ts +8 -13
- package/src/hooks/ledger-loader.ts +4 -6
- package/src/hooks/token-aware-truncation.ts +11 -17
- package/src/index.ts +54 -22
- package/src/indexing/milestone-artifact-classifier.ts +26 -0
- package/src/indexing/milestone-artifact-ingest.ts +42 -0
- package/src/octto/constants.ts +20 -0
- package/src/octto/session/browser.ts +32 -0
- package/src/octto/session/index.ts +25 -0
- package/src/octto/session/server.ts +89 -0
- package/src/octto/session/sessions.ts +383 -0
- package/src/octto/session/types.ts +305 -0
- package/src/octto/session/utils.ts +25 -0
- package/src/octto/session/waiter.ts +139 -0
- package/src/octto/state/index.ts +5 -0
- package/src/octto/state/persistence.ts +65 -0
- package/src/octto/state/store.ts +161 -0
- package/src/octto/state/types.ts +51 -0
- package/src/octto/types.ts +376 -0
- package/src/octto/ui/bundle.ts +1650 -0
- package/src/octto/ui/index.ts +2 -0
- package/src/tools/artifact-index/index.ts +152 -3
- package/src/tools/artifact-index/schema.sql +21 -0
- package/src/tools/milestone-artifact-search.ts +48 -0
- package/src/tools/octto/brainstorm.ts +332 -0
- package/src/tools/octto/extractor.ts +95 -0
- package/src/tools/octto/factory.ts +89 -0
- package/src/tools/octto/formatters.ts +63 -0
- package/src/tools/octto/index.ts +27 -0
- package/src/tools/octto/processor.ts +165 -0
- package/src/tools/octto/questions.ts +508 -0
- package/src/tools/octto/responses.ts +135 -0
- package/src/tools/octto/session.ts +114 -0
- package/src/tools/octto/types.ts +21 -0
- package/src/tools/octto/utils.ts +4 -0
- package/src/tools/pty/manager.ts +13 -7
- package/src/tools/spawn-agent.ts +1 -3
- package/src/utils/config.ts +123 -0
- package/src/utils/errors.ts +57 -0
- package/src/utils/logger.ts +50 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// src/octto/session/sessions.ts
|
|
2
|
+
import type { ServerWebSocket } from "bun";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_ANSWER_TIMEOUT_MS } from "../constants";
|
|
5
|
+
import { openBrowser } from "./browser";
|
|
6
|
+
import { createServer } from "./server";
|
|
7
|
+
import {
|
|
8
|
+
type Answer,
|
|
9
|
+
type BaseConfig,
|
|
10
|
+
type EndSessionOutput,
|
|
11
|
+
type GetAnswerInput,
|
|
12
|
+
type GetAnswerOutput,
|
|
13
|
+
type GetNextAnswerInput,
|
|
14
|
+
type GetNextAnswerOutput,
|
|
15
|
+
type ListQuestionsOutput,
|
|
16
|
+
type PushQuestionOutput,
|
|
17
|
+
type Question,
|
|
18
|
+
type QuestionType,
|
|
19
|
+
type Session,
|
|
20
|
+
STATUSES,
|
|
21
|
+
type StartSessionInput,
|
|
22
|
+
type StartSessionOutput,
|
|
23
|
+
WS_MESSAGES,
|
|
24
|
+
type WsClientMessage,
|
|
25
|
+
type WsServerMessage,
|
|
26
|
+
} from "./types";
|
|
27
|
+
import { generateQuestionId, generateSessionId } from "./utils";
|
|
28
|
+
import { createWaiters } from "./waiter";
|
|
29
|
+
|
|
30
|
+
export interface SessionStoreOptions {
|
|
31
|
+
/** Skip opening browser - useful for tests */
|
|
32
|
+
skipBrowser?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SessionStore {
|
|
36
|
+
startSession: (input: StartSessionInput) => Promise<StartSessionOutput>;
|
|
37
|
+
endSession: (sessionId: string) => Promise<EndSessionOutput>;
|
|
38
|
+
pushQuestion: (sessionId: string, type: QuestionType, config: BaseConfig) => PushQuestionOutput;
|
|
39
|
+
getAnswer: (input: GetAnswerInput) => Promise<GetAnswerOutput>;
|
|
40
|
+
getNextAnswer: (input: GetNextAnswerInput) => Promise<GetNextAnswerOutput>;
|
|
41
|
+
cancelQuestion: (questionId: string) => { ok: boolean };
|
|
42
|
+
listQuestions: (sessionId?: string) => ListQuestionsOutput;
|
|
43
|
+
handleWsConnect: (sessionId: string, ws: ServerWebSocket<unknown>) => void;
|
|
44
|
+
handleWsDisconnect: (sessionId: string) => void;
|
|
45
|
+
handleWsMessage: (sessionId: string, message: WsClientMessage) => void;
|
|
46
|
+
getSession: (sessionId: string) => Session | undefined;
|
|
47
|
+
cleanup: () => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createSessionStore(options: SessionStoreOptions = {}): SessionStore {
|
|
51
|
+
const sessions = new Map<string, Session>();
|
|
52
|
+
const questionToSession = new Map<string, string>();
|
|
53
|
+
const responseWaiters = createWaiters<string, Answer | { cancelled: true }>();
|
|
54
|
+
const sessionWaiters = createWaiters<string, { questionId: string; response: Answer }>();
|
|
55
|
+
|
|
56
|
+
const store: SessionStore = {
|
|
57
|
+
async startSession(input: StartSessionInput): Promise<StartSessionOutput> {
|
|
58
|
+
const sessionId = generateSessionId();
|
|
59
|
+
const { server, port } = await createServer(sessionId, store);
|
|
60
|
+
const urlHost = server.hostname ?? "localhost";
|
|
61
|
+
const url = `http://${urlHost}:${port}`;
|
|
62
|
+
|
|
63
|
+
const session: Session = {
|
|
64
|
+
id: sessionId,
|
|
65
|
+
title: input.title,
|
|
66
|
+
port,
|
|
67
|
+
url,
|
|
68
|
+
createdAt: new Date(),
|
|
69
|
+
questions: new Map(),
|
|
70
|
+
wsConnected: false,
|
|
71
|
+
server,
|
|
72
|
+
};
|
|
73
|
+
sessions.set(sessionId, session);
|
|
74
|
+
|
|
75
|
+
const questionIds = (input.questions ?? []).map((q) => {
|
|
76
|
+
const questionId = generateQuestionId();
|
|
77
|
+
const question: Question = {
|
|
78
|
+
id: questionId,
|
|
79
|
+
sessionId,
|
|
80
|
+
type: q.type,
|
|
81
|
+
config: q.config,
|
|
82
|
+
status: STATUSES.PENDING,
|
|
83
|
+
createdAt: new Date(),
|
|
84
|
+
};
|
|
85
|
+
session.questions.set(questionId, question);
|
|
86
|
+
questionToSession.set(questionId, sessionId);
|
|
87
|
+
return questionId;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!options.skipBrowser) {
|
|
91
|
+
await openBrowser(url).catch((error) => {
|
|
92
|
+
sessions.delete(sessionId);
|
|
93
|
+
for (const qId of questionIds) questionToSession.delete(qId);
|
|
94
|
+
server.stop();
|
|
95
|
+
throw error;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
session_id: sessionId,
|
|
101
|
+
url,
|
|
102
|
+
question_ids: questionIds.length > 0 ? questionIds : undefined,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async endSession(sessionId: string): Promise<EndSessionOutput> {
|
|
107
|
+
const session = sessions.get(sessionId);
|
|
108
|
+
if (!session) {
|
|
109
|
+
return { ok: false };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (session.wsClient) {
|
|
113
|
+
const msg: WsServerMessage = { type: WS_MESSAGES.END };
|
|
114
|
+
session.wsClient.send(JSON.stringify(msg));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (session.server) {
|
|
118
|
+
session.server.stop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const questionId of session.questions.keys()) {
|
|
122
|
+
questionToSession.delete(questionId);
|
|
123
|
+
responseWaiters.clear(questionId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sessions.delete(sessionId);
|
|
127
|
+
return { ok: true };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
pushQuestion(sessionId: string, type: QuestionType, config: BaseConfig): PushQuestionOutput {
|
|
131
|
+
const session = sessions.get(sessionId);
|
|
132
|
+
if (!session) {
|
|
133
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const questionId = generateQuestionId();
|
|
137
|
+
|
|
138
|
+
const question: Question = {
|
|
139
|
+
id: questionId,
|
|
140
|
+
sessionId,
|
|
141
|
+
type,
|
|
142
|
+
config,
|
|
143
|
+
status: STATUSES.PENDING,
|
|
144
|
+
createdAt: new Date(),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
session.questions.set(questionId, question);
|
|
148
|
+
questionToSession.set(questionId, sessionId);
|
|
149
|
+
|
|
150
|
+
if (session.wsConnected && session.wsClient) {
|
|
151
|
+
const msg: WsServerMessage = {
|
|
152
|
+
type: WS_MESSAGES.QUESTION,
|
|
153
|
+
id: questionId,
|
|
154
|
+
questionType: type,
|
|
155
|
+
config,
|
|
156
|
+
};
|
|
157
|
+
session.wsClient.send(JSON.stringify(msg));
|
|
158
|
+
} else if (!options.skipBrowser) {
|
|
159
|
+
openBrowser(session.url).catch(console.error);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { question_id: questionId };
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async getAnswer(input: GetAnswerInput): Promise<GetAnswerOutput> {
|
|
166
|
+
const sessionId = questionToSession.get(input.question_id);
|
|
167
|
+
if (!sessionId) {
|
|
168
|
+
return { completed: false, status: STATUSES.CANCELLED, reason: STATUSES.CANCELLED };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const session = sessions.get(sessionId);
|
|
172
|
+
if (!session) {
|
|
173
|
+
return { completed: false, status: STATUSES.CANCELLED, reason: STATUSES.CANCELLED };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const question = session.questions.get(input.question_id);
|
|
177
|
+
if (!question) {
|
|
178
|
+
return { completed: false, status: STATUSES.CANCELLED, reason: STATUSES.CANCELLED };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (question.status === STATUSES.ANSWERED) {
|
|
182
|
+
return { completed: true, status: STATUSES.ANSWERED, response: question.response };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (question.status === STATUSES.CANCELLED || question.status === STATUSES.TIMEOUT) {
|
|
186
|
+
return { completed: false, status: question.status, reason: question.status };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!input.block) {
|
|
190
|
+
return { completed: false, status: STATUSES.PENDING, reason: STATUSES.PENDING };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const timeout = input.timeout ?? DEFAULT_ANSWER_TIMEOUT_MS;
|
|
194
|
+
|
|
195
|
+
return new Promise<GetAnswerOutput>((resolve) => {
|
|
196
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
197
|
+
|
|
198
|
+
const cleanup = responseWaiters.register(input.question_id, (response) => {
|
|
199
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
200
|
+
if (response && typeof response === "object" && "cancelled" in response) {
|
|
201
|
+
resolve({ completed: false, status: STATUSES.CANCELLED, reason: STATUSES.CANCELLED });
|
|
202
|
+
} else {
|
|
203
|
+
resolve({ completed: true, status: STATUSES.ANSWERED, response });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
timeoutId = setTimeout(() => {
|
|
208
|
+
cleanup();
|
|
209
|
+
resolve({ completed: false, status: STATUSES.TIMEOUT, reason: STATUSES.TIMEOUT });
|
|
210
|
+
}, timeout);
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async getNextAnswer(input: GetNextAnswerInput): Promise<GetNextAnswerOutput> {
|
|
215
|
+
const session = sessions.get(input.session_id);
|
|
216
|
+
if (!session) {
|
|
217
|
+
return { completed: false, status: STATUSES.NONE_PENDING, reason: STATUSES.NONE_PENDING };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const question of session.questions.values()) {
|
|
221
|
+
if (question.status === STATUSES.ANSWERED && !question.retrieved) {
|
|
222
|
+
question.retrieved = true;
|
|
223
|
+
return {
|
|
224
|
+
completed: true,
|
|
225
|
+
question_id: question.id,
|
|
226
|
+
question_type: question.type,
|
|
227
|
+
status: STATUSES.ANSWERED,
|
|
228
|
+
response: question.response,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hasPending = Array.from(session.questions.values()).some((q) => q.status === STATUSES.PENDING);
|
|
234
|
+
|
|
235
|
+
if (!hasPending) {
|
|
236
|
+
return { completed: false, status: STATUSES.NONE_PENDING, reason: STATUSES.NONE_PENDING };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!input.block) {
|
|
240
|
+
return { completed: false, status: STATUSES.PENDING };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const timeout = input.timeout ?? DEFAULT_ANSWER_TIMEOUT_MS;
|
|
244
|
+
|
|
245
|
+
return new Promise<GetNextAnswerOutput>((resolve) => {
|
|
246
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
247
|
+
|
|
248
|
+
const cleanup = sessionWaiters.register(input.session_id, ({ questionId, response }) => {
|
|
249
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
250
|
+
const question = session.questions.get(questionId);
|
|
251
|
+
if (question) question.retrieved = true;
|
|
252
|
+
resolve({
|
|
253
|
+
completed: true,
|
|
254
|
+
question_id: questionId,
|
|
255
|
+
question_type: question?.type,
|
|
256
|
+
status: STATUSES.ANSWERED,
|
|
257
|
+
response,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
timeoutId = setTimeout(() => {
|
|
262
|
+
cleanup();
|
|
263
|
+
resolve({ completed: false, status: STATUSES.TIMEOUT, reason: STATUSES.TIMEOUT });
|
|
264
|
+
}, timeout);
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
cancelQuestion(questionId: string): { ok: boolean } {
|
|
269
|
+
const sessionId = questionToSession.get(questionId);
|
|
270
|
+
if (!sessionId) {
|
|
271
|
+
return { ok: false };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const session = sessions.get(sessionId);
|
|
275
|
+
if (!session) {
|
|
276
|
+
return { ok: false };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const question = session.questions.get(questionId);
|
|
280
|
+
if (!question || question.status !== STATUSES.PENDING) {
|
|
281
|
+
return { ok: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
question.status = STATUSES.CANCELLED;
|
|
285
|
+
|
|
286
|
+
if (session.wsClient) {
|
|
287
|
+
const msg: WsServerMessage = { type: WS_MESSAGES.CANCEL, id: questionId };
|
|
288
|
+
session.wsClient.send(JSON.stringify(msg));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
responseWaiters.notifyAll(questionId, { cancelled: true });
|
|
292
|
+
|
|
293
|
+
return { ok: true };
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
listQuestions(sessionId?: string): ListQuestionsOutput {
|
|
297
|
+
const questions: ListQuestionsOutput["questions"] = [];
|
|
298
|
+
|
|
299
|
+
const sessionsToCheck = sessionId ? [sessions.get(sessionId)].filter(Boolean) : Array.from(sessions.values());
|
|
300
|
+
|
|
301
|
+
for (const session of sessionsToCheck) {
|
|
302
|
+
if (!session) continue;
|
|
303
|
+
for (const question of session.questions.values()) {
|
|
304
|
+
questions.push({
|
|
305
|
+
id: question.id,
|
|
306
|
+
type: question.type,
|
|
307
|
+
status: question.status,
|
|
308
|
+
createdAt: question.createdAt.toISOString(),
|
|
309
|
+
answeredAt: question.answeredAt?.toISOString(),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
questions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
315
|
+
|
|
316
|
+
return { questions };
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
handleWsConnect(sessionId: string, ws: ServerWebSocket<unknown>): void {
|
|
320
|
+
const session = sessions.get(sessionId);
|
|
321
|
+
if (!session) return;
|
|
322
|
+
|
|
323
|
+
session.wsConnected = true;
|
|
324
|
+
session.wsClient = ws;
|
|
325
|
+
|
|
326
|
+
for (const question of session.questions.values()) {
|
|
327
|
+
if (question.status === STATUSES.PENDING) {
|
|
328
|
+
const msg: WsServerMessage = {
|
|
329
|
+
type: WS_MESSAGES.QUESTION,
|
|
330
|
+
id: question.id,
|
|
331
|
+
questionType: question.type,
|
|
332
|
+
config: question.config,
|
|
333
|
+
};
|
|
334
|
+
ws.send(JSON.stringify(msg));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
handleWsDisconnect(sessionId: string): void {
|
|
340
|
+
const session = sessions.get(sessionId);
|
|
341
|
+
if (!session) return;
|
|
342
|
+
|
|
343
|
+
session.wsConnected = false;
|
|
344
|
+
session.wsClient = undefined;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
handleWsMessage(sessionId: string, message: WsClientMessage): void {
|
|
348
|
+
if (message.type === WS_MESSAGES.CONNECTED) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (message.type === WS_MESSAGES.RESPONSE) {
|
|
353
|
+
const session = sessions.get(sessionId);
|
|
354
|
+
if (!session) return;
|
|
355
|
+
|
|
356
|
+
const question = session.questions.get(message.id);
|
|
357
|
+
if (!question || question.status !== STATUSES.PENDING) return;
|
|
358
|
+
|
|
359
|
+
question.status = STATUSES.ANSWERED;
|
|
360
|
+
question.answeredAt = new Date();
|
|
361
|
+
question.response = message.answer;
|
|
362
|
+
|
|
363
|
+
responseWaiters.notifyAll(message.id, message.answer);
|
|
364
|
+
sessionWaiters.notifyFirst(sessionId, {
|
|
365
|
+
questionId: message.id,
|
|
366
|
+
response: message.answer,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
getSession(sessionId: string): Session | undefined {
|
|
372
|
+
return sessions.get(sessionId);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async cleanup(): Promise<void> {
|
|
376
|
+
for (const sessionId of sessions.keys()) {
|
|
377
|
+
await store.endSession(sessionId);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return store;
|
|
383
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/octto/session/types.ts
|
|
2
|
+
// Session and Question types for the octto module
|
|
3
|
+
import type { ServerWebSocket } from "bun";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AskCodeConfig,
|
|
7
|
+
AskFileConfig,
|
|
8
|
+
AskImageConfig,
|
|
9
|
+
AskTextConfig,
|
|
10
|
+
ConfirmConfig,
|
|
11
|
+
EmojiReactConfig,
|
|
12
|
+
PickManyConfig,
|
|
13
|
+
PickOneConfig,
|
|
14
|
+
RankConfig,
|
|
15
|
+
RateConfig,
|
|
16
|
+
ReviewSectionConfig,
|
|
17
|
+
ShowDiffConfig,
|
|
18
|
+
ShowOptionsConfig,
|
|
19
|
+
ShowPlanConfig,
|
|
20
|
+
SliderConfig,
|
|
21
|
+
ThumbsConfig,
|
|
22
|
+
} from "../types";
|
|
23
|
+
|
|
24
|
+
export const STATUSES = {
|
|
25
|
+
PENDING: "pending",
|
|
26
|
+
ANSWERED: "answered",
|
|
27
|
+
CANCELLED: "cancelled",
|
|
28
|
+
TIMEOUT: "timeout",
|
|
29
|
+
NONE_PENDING: "none_pending",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type QuestionStatus = (typeof STATUSES)[Exclude<keyof typeof STATUSES, "NONE_PENDING">];
|
|
33
|
+
|
|
34
|
+
export interface Question {
|
|
35
|
+
id: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
type: QuestionType;
|
|
38
|
+
config: BaseConfig;
|
|
39
|
+
status: QuestionStatus;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
answeredAt?: Date;
|
|
42
|
+
response?: Answer;
|
|
43
|
+
/** True if this answer was already returned via get_next_answer */
|
|
44
|
+
retrieved?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const QUESTIONS = {
|
|
48
|
+
PICK_ONE: "pick_one",
|
|
49
|
+
PICK_MANY: "pick_many",
|
|
50
|
+
CONFIRM: "confirm",
|
|
51
|
+
RANK: "rank",
|
|
52
|
+
RATE: "rate",
|
|
53
|
+
ASK_TEXT: "ask_text",
|
|
54
|
+
ASK_IMAGE: "ask_image",
|
|
55
|
+
ASK_FILE: "ask_file",
|
|
56
|
+
ASK_CODE: "ask_code",
|
|
57
|
+
SHOW_DIFF: "show_diff",
|
|
58
|
+
SHOW_PLAN: "show_plan",
|
|
59
|
+
SHOW_OPTIONS: "show_options",
|
|
60
|
+
REVIEW_SECTION: "review_section",
|
|
61
|
+
THUMBS: "thumbs",
|
|
62
|
+
EMOJI_REACT: "emoji_react",
|
|
63
|
+
SLIDER: "slider",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
export type QuestionType = (typeof QUESTIONS)[keyof typeof QUESTIONS];
|
|
67
|
+
export const QUESTION_TYPES = Object.values(QUESTIONS);
|
|
68
|
+
|
|
69
|
+
// --- Answer Types ---
|
|
70
|
+
|
|
71
|
+
export interface PickOneAnswer {
|
|
72
|
+
selected: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PickManyAnswer {
|
|
76
|
+
selected: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ConfirmAnswer {
|
|
80
|
+
choice: "yes" | "no" | "cancel";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ThumbsAnswer {
|
|
84
|
+
choice: "up" | "down";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface EmojiReactAnswer {
|
|
88
|
+
emoji: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AskTextAnswer {
|
|
92
|
+
text: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface SliderAnswer {
|
|
96
|
+
value: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RankAnswer {
|
|
100
|
+
ranking: Array<{ id: string; rank: number }>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RateAnswer {
|
|
104
|
+
ratings: Record<string, number>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface AskCodeAnswer {
|
|
108
|
+
code: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface AskImageAnswer {
|
|
112
|
+
images: Array<{ name: string; data: string; type: string }>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface AskFileAnswer {
|
|
116
|
+
files: Array<{ name: string; data: string; type: string }>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ReviewAnswer {
|
|
120
|
+
decision: string;
|
|
121
|
+
feedback?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ShowOptionsAnswer {
|
|
125
|
+
selected: string;
|
|
126
|
+
feedback?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type Answer =
|
|
130
|
+
| PickOneAnswer
|
|
131
|
+
| PickManyAnswer
|
|
132
|
+
| ConfirmAnswer
|
|
133
|
+
| ThumbsAnswer
|
|
134
|
+
| EmojiReactAnswer
|
|
135
|
+
| AskTextAnswer
|
|
136
|
+
| SliderAnswer
|
|
137
|
+
| RankAnswer
|
|
138
|
+
| RateAnswer
|
|
139
|
+
| AskCodeAnswer
|
|
140
|
+
| AskImageAnswer
|
|
141
|
+
| AskFileAnswer
|
|
142
|
+
| ReviewAnswer
|
|
143
|
+
| ShowOptionsAnswer;
|
|
144
|
+
|
|
145
|
+
export interface QuestionAnswers {
|
|
146
|
+
[QUESTIONS.PICK_ONE]: PickOneAnswer;
|
|
147
|
+
[QUESTIONS.PICK_MANY]: PickManyAnswer;
|
|
148
|
+
[QUESTIONS.CONFIRM]: ConfirmAnswer;
|
|
149
|
+
[QUESTIONS.THUMBS]: ThumbsAnswer;
|
|
150
|
+
[QUESTIONS.EMOJI_REACT]: EmojiReactAnswer;
|
|
151
|
+
[QUESTIONS.ASK_TEXT]: AskTextAnswer;
|
|
152
|
+
[QUESTIONS.SLIDER]: SliderAnswer;
|
|
153
|
+
[QUESTIONS.RANK]: RankAnswer;
|
|
154
|
+
[QUESTIONS.RATE]: RateAnswer;
|
|
155
|
+
[QUESTIONS.ASK_CODE]: AskCodeAnswer;
|
|
156
|
+
[QUESTIONS.ASK_IMAGE]: AskImageAnswer;
|
|
157
|
+
[QUESTIONS.ASK_FILE]: AskFileAnswer;
|
|
158
|
+
[QUESTIONS.SHOW_DIFF]: ReviewAnswer;
|
|
159
|
+
[QUESTIONS.SHOW_PLAN]: ReviewAnswer;
|
|
160
|
+
[QUESTIONS.REVIEW_SECTION]: ReviewAnswer;
|
|
161
|
+
[QUESTIONS.SHOW_OPTIONS]: ShowOptionsAnswer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type QuestionConfig =
|
|
165
|
+
| PickOneConfig
|
|
166
|
+
| PickManyConfig
|
|
167
|
+
| ConfirmConfig
|
|
168
|
+
| RankConfig
|
|
169
|
+
| RateConfig
|
|
170
|
+
| AskTextConfig
|
|
171
|
+
| AskImageConfig
|
|
172
|
+
| AskFileConfig
|
|
173
|
+
| AskCodeConfig
|
|
174
|
+
| ShowDiffConfig
|
|
175
|
+
| ShowPlanConfig
|
|
176
|
+
| ShowOptionsConfig
|
|
177
|
+
| ReviewSectionConfig
|
|
178
|
+
| ThumbsConfig
|
|
179
|
+
| EmojiReactConfig
|
|
180
|
+
| SliderConfig;
|
|
181
|
+
|
|
182
|
+
/** Config type for transit - accepts both strict QuestionConfig and loose objects */
|
|
183
|
+
export type BaseConfig =
|
|
184
|
+
| QuestionConfig
|
|
185
|
+
| {
|
|
186
|
+
question?: string;
|
|
187
|
+
context?: string;
|
|
188
|
+
[key: string]: unknown;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export interface Session {
|
|
192
|
+
id: string;
|
|
193
|
+
title?: string;
|
|
194
|
+
port: number;
|
|
195
|
+
url: string;
|
|
196
|
+
createdAt: Date;
|
|
197
|
+
questions: Map<string, Question>;
|
|
198
|
+
wsConnected: boolean;
|
|
199
|
+
server?: ReturnType<typeof Bun.serve>;
|
|
200
|
+
wsClient?: ServerWebSocket<unknown>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface InitialQuestion {
|
|
204
|
+
type: QuestionType;
|
|
205
|
+
config: BaseConfig;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface StartSessionInput {
|
|
209
|
+
title?: string;
|
|
210
|
+
/** Initial questions to display immediately when browser opens */
|
|
211
|
+
questions?: InitialQuestion[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface StartSessionOutput {
|
|
215
|
+
session_id: string;
|
|
216
|
+
url: string;
|
|
217
|
+
/** IDs of initial questions if any were provided */
|
|
218
|
+
question_ids?: string[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface EndSessionOutput {
|
|
222
|
+
ok: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface PushQuestionOutput {
|
|
226
|
+
question_id: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface GetAnswerInput {
|
|
230
|
+
question_id: string;
|
|
231
|
+
block?: boolean;
|
|
232
|
+
timeout?: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface GetAnswerOutput {
|
|
236
|
+
completed: boolean;
|
|
237
|
+
status: QuestionStatus;
|
|
238
|
+
response?: Answer;
|
|
239
|
+
reason?: "timeout" | "cancelled" | "pending";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface GetNextAnswerInput {
|
|
243
|
+
session_id: string;
|
|
244
|
+
block?: boolean;
|
|
245
|
+
timeout?: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type AnswerStatus = (typeof STATUSES)[keyof typeof STATUSES];
|
|
249
|
+
|
|
250
|
+
export interface GetNextAnswerOutput {
|
|
251
|
+
completed: boolean;
|
|
252
|
+
question_id?: string;
|
|
253
|
+
question_type?: QuestionType;
|
|
254
|
+
status: AnswerStatus;
|
|
255
|
+
response?: Answer;
|
|
256
|
+
reason?: typeof STATUSES.TIMEOUT | typeof STATUSES.NONE_PENDING;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ListQuestionsOutput {
|
|
260
|
+
questions: Array<{
|
|
261
|
+
id: string;
|
|
262
|
+
type: QuestionType;
|
|
263
|
+
status: QuestionStatus;
|
|
264
|
+
createdAt: string;
|
|
265
|
+
answeredAt?: string;
|
|
266
|
+
}>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// WebSocket message types
|
|
270
|
+
export const WS_MESSAGES = {
|
|
271
|
+
QUESTION: "question",
|
|
272
|
+
CANCEL: "cancel",
|
|
273
|
+
END: "end",
|
|
274
|
+
RESPONSE: "response",
|
|
275
|
+
CONNECTED: "connected",
|
|
276
|
+
} as const;
|
|
277
|
+
|
|
278
|
+
export interface WsQuestionMessage {
|
|
279
|
+
type: "question";
|
|
280
|
+
id: string;
|
|
281
|
+
questionType: QuestionType;
|
|
282
|
+
config: BaseConfig;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface WsCancelMessage {
|
|
286
|
+
type: "cancel";
|
|
287
|
+
id: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface WsEndMessage {
|
|
291
|
+
type: "end";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface WsResponseMessage {
|
|
295
|
+
type: "response";
|
|
296
|
+
id: string;
|
|
297
|
+
answer: Answer;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface WsConnectedMessage {
|
|
301
|
+
type: "connected";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type WsServerMessage = WsQuestionMessage | WsCancelMessage | WsEndMessage;
|
|
305
|
+
export type WsClientMessage = WsResponseMessage | WsConnectedMessage;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/octto/session/utils.ts
|
|
2
|
+
// ID generation utilities for octto sessions and questions
|
|
3
|
+
|
|
4
|
+
const ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
5
|
+
const ID_LENGTH = 8;
|
|
6
|
+
|
|
7
|
+
function generateId(prefix: string): string {
|
|
8
|
+
let result = `${prefix}_`;
|
|
9
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
10
|
+
result += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS.length));
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generateSessionId(): string {
|
|
16
|
+
return generateId("ses");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateQuestionId(): string {
|
|
20
|
+
return generateId("q");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateBrainstormId(): string {
|
|
24
|
+
return generateId("bs");
|
|
25
|
+
}
|