Let a user stop a bot, and say the app is still alpha
The thinking row and the header menu both carry Stop while a bot has work open for the user anywhere: every pending or processing job of that bot in their conversations is failed as stopped, its task closed, a Stopped step recorded and one closing message posted per session, so the thread stops waiting and the bot goes idle. The handler running the job is aborted from this process through a registry, so a job stuck waiting on a model stops at once instead of on its next heartbeat, and a late face change after the abort is dropped. The release stage is now said in the app too: Settings > General reports "Cobble v0.1.5 (alpha)" next to development, and the README and RELEASING.md say what alpha means for installs.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotGettingMessageStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import botBusyState from "./bot-busy-state";
|
||||
|
||||
function bot(overrides: Partial<BotWithLastMessage>): BotWithLastMessage {
|
||||
return { id: 1, name: "Ember", ...overrides } as BotWithLastMessage;
|
||||
}
|
||||
|
||||
describe("botBusyState", () => {
|
||||
test("a reply owed here is thinking", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "idle" }), awaiting_reply: true });
|
||||
|
||||
expect(state.thinking).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("a message still on its way is getting, not thinking", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working" }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.thinking).toBe(false);
|
||||
});
|
||||
|
||||
test("a task running while the bot has the last word is working", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: "Working" }),
|
||||
awaiting_reply: false,
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.working).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("a reply's own turn never shows as working on a task", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: BotReplyJobTitle }),
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.working).toBe(false);
|
||||
expect(state.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("waiting for a model is a wait, with a reply owed here", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
awaiting_reply: true,
|
||||
});
|
||||
|
||||
expect(state.waiting).toBe(true);
|
||||
expect(state.thinking).toBe(false);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("waiting for a model is a wait, with a task running", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
has_task: true,
|
||||
});
|
||||
|
||||
expect(state.waiting).toBe(true);
|
||||
expect(state.busy).toBe(true);
|
||||
});
|
||||
|
||||
test("waiting for its computer is a wait only while a task runs", () => {
|
||||
const waiting = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotComputerBusyStatusLine }),
|
||||
has_task: true,
|
||||
});
|
||||
const idle = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotComputerBusyStatusLine }),
|
||||
});
|
||||
|
||||
expect(waiting.waiting).toBe(true);
|
||||
expect(idle.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("a message still on its way wins over the wait, so the row says what the user just did", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: BotWaitingStatusLine }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.waiting).toBe(true);
|
||||
});
|
||||
|
||||
test("an idle bot with nothing owed and no task shows nothing", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "idle" }) });
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
});
|
||||
|
||||
test("a done face with no task shows nothing", () => {
|
||||
expect(botBusyState({ bot: bot({ face: "done" }) }).busy).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("botBusyState stoppable", () => {
|
||||
test("busy in this thread is stoppable", () => {
|
||||
expect(
|
||||
botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: true,
|
||||
}).stoppable,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("working somewhere else is stoppable too, since the face is per user", () => {
|
||||
const state = botBusyState({ bot: bot({ face: "working" }) });
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
expect(state.stoppable).toBe(true);
|
||||
});
|
||||
|
||||
test("asleep on a wait line is stoppable, even when this thread shows nothing", () => {
|
||||
for (const status_line of [BotWaitingStatusLine, BotComputerBusyStatusLine]) {
|
||||
expect(botBusyState({ bot: bot({ face: "asleep", status_line }) }).stoppable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("a bot simply resting is not stoppable", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "asleep", status_line: "Until tomorrow" }),
|
||||
});
|
||||
|
||||
expect(state.busy).toBe(false);
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("a message only being collected is not stoppable: its reply is queued after it", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("done, needs-you and idle mean nothing is running", () => {
|
||||
for (const face of ["done", "needs-you", "idle"] as const) {
|
||||
const state = botBusyState({ bot: bot({ face }) });
|
||||
|
||||
expect(state.stoppable).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("a silently stopped bot is not stoppable", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle" }),
|
||||
awaiting_reply: false,
|
||||
has_task: false,
|
||||
});
|
||||
|
||||
expect(state.stoppable).toBe(false);
|
||||
});
|
||||
|
||||
test("a reply job's own turn with no task owed is not stoppable", () => {
|
||||
/**
|
||||
* The face says working while a reply runs somewhere else, which is
|
||||
* real work: it's stoppable even though this thread owes nothing.
|
||||
*/
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "working", status_line: BotReplyJobTitle }),
|
||||
});
|
||||
|
||||
expect(state.stoppable).toBe(true);
|
||||
});
|
||||
|
||||
test("the getting line still labels the row", () => {
|
||||
const state = botBusyState({
|
||||
bot: bot({ face: "idle", status_line: BotGettingMessageStatusLine }),
|
||||
awaiting_reply: true,
|
||||
receiving: true,
|
||||
});
|
||||
|
||||
expect(state.getting).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
|
||||
type Params = {
|
||||
bot: BotWithLastMessage;
|
||||
/**
|
||||
* The last message in the open thread is the user's, so a reply is due
|
||||
* here.
|
||||
*/
|
||||
awaiting_reply?: boolean;
|
||||
/**
|
||||
* A message is still on its way to the server.
|
||||
*/
|
||||
receiving?: boolean;
|
||||
/**
|
||||
* A task of the bot's is running.
|
||||
*/
|
||||
has_task?: boolean;
|
||||
};
|
||||
|
||||
export type BotBusyState = {
|
||||
/**
|
||||
* Waiting for a model to come back, or for the bot's computer.
|
||||
*/
|
||||
waiting: boolean;
|
||||
/**
|
||||
* The user's message hasn't reached the server yet.
|
||||
*/
|
||||
getting: boolean;
|
||||
/**
|
||||
* A reply is due in this thread.
|
||||
*/
|
||||
thinking: boolean;
|
||||
/**
|
||||
* A task is running and the bot has the last word.
|
||||
*/
|
||||
working: boolean;
|
||||
/**
|
||||
* Anything of the above, so the row has something to say.
|
||||
*/
|
||||
busy: boolean;
|
||||
/**
|
||||
* There's work of the bot's to pull the plug on. A message still on
|
||||
* its way doesn't count: its reply is queued after it lands, so a stop
|
||||
* now would cancel nothing and be overtaken a moment later. The face
|
||||
* covers work in the user's other conversations too, since it's one
|
||||
* per user; `asleep` only counts on the two status lines the queue
|
||||
* sets while it waits, never a bot that's simply resting.
|
||||
*/
|
||||
stoppable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a bot is doing in a conversation, read from the thread and its face.
|
||||
* The thread decides the reply states: every user message queues one, so
|
||||
* while the user has the last word a reply is due here, even while the bot
|
||||
* works elsewhere. Only the waits (for a model, for its computer) come from
|
||||
* the face. The thinking row reads it for its line, and the conversation
|
||||
* reads it for the stop control.
|
||||
*/
|
||||
export default function botBusyState({
|
||||
bot,
|
||||
awaiting_reply,
|
||||
receiving,
|
||||
has_task,
|
||||
}: Params): BotBusyState {
|
||||
const asleep = bot.face == "asleep";
|
||||
const waiting_model =
|
||||
asleep && bot.status_line == BotWaitingStatusLine && Boolean(awaiting_reply || has_task);
|
||||
const waiting_computer =
|
||||
asleep &&
|
||||
bot.status_line == BotComputerBusyStatusLine &&
|
||||
Boolean(has_task) &&
|
||||
!awaiting_reply;
|
||||
const waiting = waiting_model || waiting_computer;
|
||||
const getting = Boolean(receiving) && Boolean(awaiting_reply);
|
||||
const thinking = Boolean(awaiting_reply) && !waiting && !getting;
|
||||
const working =
|
||||
!awaiting_reply &&
|
||||
bot.face == "working" &&
|
||||
bot.status_line != BotReplyJobTitle &&
|
||||
Boolean(has_task);
|
||||
|
||||
/**
|
||||
* Work runs here (a reply owed, a task of this thread's) or somewhere
|
||||
* else of this user's (the face says `working`, or `asleep` on a wait
|
||||
* line).
|
||||
*/
|
||||
const stoppable =
|
||||
waiting ||
|
||||
thinking ||
|
||||
working ||
|
||||
bot.face == "working" ||
|
||||
(asleep &&
|
||||
(bot.status_line == BotWaitingStatusLine ||
|
||||
bot.status_line == BotComputerBusyStatusLine));
|
||||
|
||||
return {
|
||||
waiting,
|
||||
getting,
|
||||
thinking,
|
||||
working,
|
||||
busy: getting || thinking || working || waiting,
|
||||
stoppable,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user