Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 5x 5x 5x 5x 19x 95x 19x 19x 19x 19x 19x 17x 2x 5x 5x 5x 5x 20x 20x 20x 17x 20x 16x 11x 11x 2x 5x 173x 72x 65x 2x 2x 3x 144x 144x 142x 142x 141x | import { Notice, type Vault } from "obsidian";
import { MESSAGES } from "../locales/en";
import { node } from "../node";
import { errorMessage } from "../utils";
import { SEARCH_CHUNK_TYPE, type SearchChunkType, type SessionExport } from "../types";
/** The server's `SearchScope` vocabulary. It says "both" where the plugin says "all". */
export type SessionScope = "raw" | "wiki" | "both";
export const SESSION_SCOPE = {
RAW: "raw",
WIKI: "wiki",
BOTH: "both",
} as const satisfies Record<string, SessionScope>;
/** Longest auto-derived title before it gets an ellipsis. Matches the server's TITLE_MAX_LEN. */
export const SESSION_TITLE_MAX_LEN = 60;
const TITLE_ELLIPSIS = "…";
/** Vault folder that "Save to vault" writes chats into. */
const CHAT_NOTE_FOLDER = "lilbee";
/** Write `content` as a new timestamped note in the chat folder and say where, or say it failed. */
export async function saveChatNote(vault: Vault, content: string): Promise<void> {
const now = new Date();
const pad = (n: number): string => String(n).padStart(2, "0");
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
const path = `${CHAT_NOTE_FOLDER}/chat-${stamp}.md`;
try {
if (!vault.getAbstractFileByPath(CHAT_NOTE_FOLDER)) await vault.createFolder(CHAT_NOTE_FOLDER);
await vault.create(path, content);
new Notice(MESSAGES.NOTICE_SAVED(path));
} catch {
new Notice(MESSAGES.ERROR_SAVE_CHAT);
}
}
/** Icon on every "Save to vault" action: the chat toolbar and the sessions list rows. */
export const SAVE_ICON = "save";
/** Icon on every "Export chat" action: the chat toolbar and the sessions list rows. */
export const EXPORT_ICON = "download";
/** Extension of an exported chat file. */
export const CHAT_EXPORT_EXTENSION = "md";
/** Stem of an exported chat's file name when the server names none. */
const EXPORT_FALLBACK_STEM = "chat";
/** Get the export, ask where to write it under the server's file name, then write it and say where; a null export or a cancel writes nothing. */
export async function exportChatFile(
choosePath: (defaultName: string) => Promise<string | null>,
content: () => Promise<SessionExport | null>,
sessionId: string | null,
): Promise<void> {
try {
const exported = await content();
if (exported === null) return;
const stem = sessionId ? `${EXPORT_FALLBACK_STEM}-${sessionId}` : EXPORT_FALLBACK_STEM;
const path = await choosePath(exported.fileName ?? `${stem}.${CHAT_EXPORT_EXTENSION}`);
if (!path) return;
node.writeFileSync(path, exported.markdown);
new Notice(MESSAGES.NOTICE_CHAT_EXPORTED(path));
} catch (err) {
new Notice(MESSAGES.ERROR_CHAT_EXPORT_FAILED(errorMessage(err, MESSAGES.ERROR_UNKNOWN)));
}
}
/** Icon on every fork action: the sessions list rows and the chat's answers. */
export const FORK_ICON = "git-fork";
/** Translate the plugin's chunk-type selection into a scope the server can parse. */
export function scopeFromChunkType(chunk: SearchChunkType): SessionScope {
return chunk === SEARCH_CHUNK_TYPE.ALL ? SESSION_SCOPE.BOTH : chunk;
}
/** Inverse of `scopeFromChunkType`. Null for a scope this build doesn't know. */
export function chunkTypeFromScope(scope: string): SearchChunkType | null {
switch (scope) {
case SESSION_SCOPE.BOTH:
return SEARCH_CHUNK_TYPE.ALL;
case SESSION_SCOPE.WIKI:
return SEARCH_CHUNK_TYPE.WIKI;
case SESSION_SCOPE.RAW:
return SEARCH_CHUNK_TYPE.RAW;
default:
return null;
}
}
/** Title a session from its first user message: first line, truncated. Mirrors `derive_title`. */
export function deriveSessionTitle(text: string): string {
const stripped = text.trim();
if (!stripped) return MESSAGES.SESSIONS_UNTITLED;
const first = stripped.split(/\r\n|[\r\n]/)[0];
if (first.length > SESSION_TITLE_MAX_LEN) return first.slice(0, SESSION_TITLE_MAX_LEN) + TITLE_ELLIPSIS;
return first;
}
|