feat: snip a region into chat, and make links clickable (#14)
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import type { PendingAttachment, Session, SessionSummary } from "./types";
|
||||
import { errorMessage } from "./utils";
|
||||
|
||||
/**
|
||||
* Everything the chat view resolves before its first render.
|
||||
*
|
||||
* Pulled out of the component and made pure because this is the part that keeps
|
||||
* going wrong. Three things can happen on open, and all of them are
|
||||
* asynchronous: a launch hands over a captured file, it may also name the
|
||||
* conversation to return to, and an ordinary open resumes the most recent
|
||||
* conversation if it is fresh enough. Every path has to end in a rendered chat.
|
||||
*
|
||||
* As a plain function it is testable without mounting anything, which matters
|
||||
* because the alternative is finding out from a screenshot that a view is
|
||||
* spinning forever.
|
||||
*
|
||||
* Imports nothing from Vicinae, so it belongs to the logic layer.
|
||||
*/
|
||||
|
||||
export interface StartupInput {
|
||||
/** A conversation already in hand, which is how the sessions command pushes this view. */
|
||||
hasInitialSession: boolean;
|
||||
/** A conversation to reopen by id, handed over by the snip command. */
|
||||
initialSessionId?: string;
|
||||
/** A captured image to read in. */
|
||||
initialAttachmentPath?: string;
|
||||
/** Idle time past which an ordinary open starts fresh. Undefined means never time out. */
|
||||
resumeTimeoutMs?: number;
|
||||
/** Injected so the resume window is deterministic under test. */
|
||||
now: number;
|
||||
}
|
||||
|
||||
/** The side effects, injected so nothing here touches storage or the file system directly. */
|
||||
export interface StartupPorts {
|
||||
loadSession: (id: string) => Promise<Session | undefined>;
|
||||
listSessions: () => Promise<SessionSummary[]>;
|
||||
readAttachment: (path: string) => Promise<PendingAttachment>;
|
||||
}
|
||||
|
||||
export interface StartupResult {
|
||||
/** The conversation to show, when one was restored. */
|
||||
session?: Session;
|
||||
/** The image that was handed over, when one could be read. */
|
||||
attachment?: PendingAttachment;
|
||||
/** Why a handed-over image could not be read, so the view can say so. */
|
||||
attachmentError?: string;
|
||||
}
|
||||
|
||||
/** True when the launch handed something over, which also means "do not resume". */
|
||||
export function isHandoff(input: Pick<StartupInput, "initialSessionId" | "initialAttachmentPath">): boolean {
|
||||
return Boolean(input.initialSessionId || input.initialAttachmentPath);
|
||||
}
|
||||
|
||||
export async function resolveStartup(input: StartupInput, ports: StartupPorts): Promise<StartupResult> {
|
||||
const result: StartupResult = {};
|
||||
|
||||
if (input.initialSessionId) {
|
||||
// A hand-off names the conversation to return to, so that one is restored
|
||||
// rather than whichever chat happens to be most recent. By id matters
|
||||
// because taking a snip is not a turn, so nothing was saved in between.
|
||||
// A session that has since been deleted just means starting fresh.
|
||||
result.session = await ports.loadSession(input.initialSessionId).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (input.initialAttachmentPath) {
|
||||
try {
|
||||
result.attachment = await ports.readAttachment(input.initialAttachmentPath);
|
||||
} catch (thrown) {
|
||||
// A missing file leaves an empty chat with an explanation, which beats
|
||||
// refusing to open at all.
|
||||
result.attachmentError = errorMessage(thrown);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing was handed over, so the idle-resume rule applies: reopening the
|
||||
// command continues the most recent conversation, but only if it was used
|
||||
// recently. The previous conversation is never removed either way, it just
|
||||
// drops down the sidebar.
|
||||
if (!input.hasInitialSession && !isHandoff(input)) {
|
||||
const latest = await ports
|
||||
.listSessions()
|
||||
.then((sessions) => sessions[0])
|
||||
.catch(() => undefined);
|
||||
|
||||
if (latest) {
|
||||
const idleMs = input.now - latest.updatedAt;
|
||||
const fresh = input.resumeTimeoutMs === undefined || idleMs <= input.resumeTimeoutMs;
|
||||
|
||||
if (fresh) {
|
||||
result.session = await ports.loadSession(latest.id).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user