← octto
import type { ServerWebSocket } from "bun";
import * as v from "valibot";
import type {
AskCodeConfig,
AskFileConfig,
AskImageConfig,
AskTextConfig,
ConfirmConfig,
EmojiReactConfig,
PickManyConfig,
PickOneConfig,
RankConfig,
RateConfig,
ReviewSectionConfig,
ShowDiffConfig,
ShowOptionsConfig,
ShowPlanConfig,
SliderConfig,
ThumbsConfig,
} from "@/types";
export const STATUSES = {
PENDING: "pending",
ANSWERED: "answered",
CANCELLED: "cancelled",
TIMEOUT: "timeout",
NONE_PENDING: "none_pending",
} as const;
export type QuestionStatus = (typeof STATUSES)[Exclude<keyof typeof STATUSES, "NONE_PENDING">];
export interface Question {
readonly id: string;
readonly sessionId: string;
readonly type: QuestionType;
readonly config: BaseConfig;
status: QuestionStatus;
readonly createdAt: Date;
answeredAt?: Date;
response?: Answer;
/** True if this answer was already returned via get_next_answer */
retrieved?: boolean;
}
export const QUESTIONS = {
PICK_ONE: "pick_one",
PICK_MANY: "pick_many",
CONFIRM: "confirm",
RANK: "rank",
RATE: "rate",
ASK_TEXT: "ask_text",
ASK_IMAGE: "ask_image",
ASK_FILE: "ask_file",
ASK_CODE: "ask_code",
SHOW_DIFF: "show_diff",
SHOW_PLAN: "show_plan",
SHOW_OPTIONS: "show_options",
REVIEW_SECTION: "review_section",
THUMBS: "thumbs",
EMOJI_REACT: "emoji_react",
SLIDER: "slider",
} as const;
export type QuestionType = (typeof QUESTIONS)[keyof typeof QUESTIONS];
export const QUESTION_TYPES = Object.values(QUESTIONS);
export interface PickOneAnswer {
selected: string;
/** Freetext supplied when `selected` is the reserved "other" id */
other?: string;
}
export interface PickManyAnswer {
selected: string[];
/** Freetext supplied when `selected` contains the reserved "other" id */
other?: string;
}
export interface ConfirmAnswer {
choice: "yes" | "no" | "cancel";
}
export interface ThumbsAnswer {
choice: "up" | "down";
}
export interface EmojiReactAnswer {
emoji: string;
}
export interface AskTextAnswer {
text: string;
}
export interface SliderAnswer {
value: number;
}
export interface RankAnswer {
ranking: Array<{ id: string; rank: number }>;
}
export interface RateAnswer {
ratings: Record<string, number>;
}
export interface AskCodeAnswer {
code: string;
}
export interface AskImageAnswer {
images: Array<{ name: string; content: string; type: string }>;
}
export interface AskFileAnswer {
files: Array<{ name: string; content: string; type: string }>;
}
export interface ReviewAnswer {
decision: string;
feedback?: string;
}
export interface ShowOptionsAnswer {
selected: string;
feedback?: string;
}
export type Answer =
| PickOneAnswer
| PickManyAnswer
| ConfirmAnswer
| ThumbsAnswer
| EmojiReactAnswer
| AskTextAnswer
| SliderAnswer
| RankAnswer
| RateAnswer
| AskCodeAnswer
| AskImageAnswer
| AskFileAnswer
| ReviewAnswer
| ShowOptionsAnswer;
export interface QuestionAnswers {
[QUESTIONS.PICK_ONE]: PickOneAnswer;
[QUESTIONS.PICK_MANY]: PickManyAnswer;
[QUESTIONS.CONFIRM]: ConfirmAnswer;
[QUESTIONS.THUMBS]: ThumbsAnswer;
[QUESTIONS.EMOJI_REACT]: EmojiReactAnswer;
[QUESTIONS.ASK_TEXT]: AskTextAnswer;
[QUESTIONS.SLIDER]: SliderAnswer;
[QUESTIONS.RANK]: RankAnswer;
[QUESTIONS.RATE]: RateAnswer;
[QUESTIONS.ASK_CODE]: AskCodeAnswer;
[QUESTIONS.ASK_IMAGE]: AskImageAnswer;
[QUESTIONS.ASK_FILE]: AskFileAnswer;
[QUESTIONS.SHOW_DIFF]: ReviewAnswer;
[QUESTIONS.SHOW_PLAN]: ReviewAnswer;
[QUESTIONS.REVIEW_SECTION]: ReviewAnswer;
[QUESTIONS.SHOW_OPTIONS]: ShowOptionsAnswer;
}
export type QuestionConfig =
| PickOneConfig
| PickManyConfig
| ConfirmConfig
| RankConfig
| RateConfig
| AskTextConfig
| AskImageConfig
| AskFileConfig
| AskCodeConfig
| ShowDiffConfig
| ShowPlanConfig
| ShowOptionsConfig
| ReviewSectionConfig
| ThumbsConfig
| EmojiReactConfig
| SliderConfig;
/** Config type for transit - accepts both strict QuestionConfig and loose objects */
export type BaseConfig =
| QuestionConfig
| {
question?: string;
context?: string;
[key: string]: unknown;
};
export interface Session {
readonly id: string;
readonly title?: string;
readonly port: number;
readonly url: string;
readonly createdAt: Date;
questions: Map<string, Question>;
wsConnected: boolean;
server?: ReturnType<typeof Bun.serve>;
wsClient?: ServerWebSocket<unknown>;
}
export interface InitialQuestion {
type: QuestionType;
config: BaseConfig;
}
export interface StartSessionInput {
title?: string;
/** Initial questions to display immediately when browser opens */
questions?: InitialQuestion[];
}
export interface StartSessionOutput {
session_id: string;
url: string;
/** IDs of initial questions if any were provided */
question_ids?: string[];
}
export interface EndSessionOutput {
ok: boolean;
}
export interface PushQuestionOutput {
question_id: string;
}
export interface GetAnswerInput {
question_id: string;
block?: boolean;
timeout?: number;
}
export interface GetAnswerOutput {
completed: boolean;
status: QuestionStatus;
response?: Answer;
reason?: "timeout" | "cancelled" | "pending";
}
export interface GetNextAnswerInput {
session_id: string;
block?: boolean;
timeout?: number;
}
export type AnswerStatus = (typeof STATUSES)[keyof typeof STATUSES];
export interface GetNextAnswerOutput {
completed: boolean;
question_id?: string;
question_type?: QuestionType;
status: AnswerStatus;
response?: Answer;
reason?: typeof STATUSES.TIMEOUT | typeof STATUSES.NONE_PENDING;
}
export interface ListQuestionsOutput {
questions: Array<{
id: string;
type: QuestionType;
status: QuestionStatus;
createdAt: string;
answeredAt?: string;
}>;
}
export const WS_MESSAGES = {
QUESTION: "question",
CANCEL: "cancel",
END: "end",
RESPONSE: "response",
CONNECTED: "connected",
} as const;
export interface WsQuestionMessage {
type: "question";
id: string;
questionType: QuestionType;
config: BaseConfig;
}
export interface WsCancelMessage {
type: "cancel";
id: string;
}
export interface WsEndMessage {
type: "end";
}
export interface WsResponseMessage {
type: "response";
id: string;
answer: Answer;
}
export interface WsConnectedMessage {
type: "connected";
}
export type WsServerMessage = WsQuestionMessage | WsCancelMessage | WsEndMessage;
export type WsClientMessage = WsResponseMessage | WsConnectedMessage;
export const WsClientMessageSchema = v.variant("type", [
v.object({
type: v.literal("response"),
id: v.string(),
answer: v.pipe(
v.unknown(),
v.transform((input): Answer => input as Answer),
),
}),
v.object({ type: v.literal("connected") }),
]);