bunnyquery 1.3.4 → 1.4.0

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/dist/engine.d.mts CHANGED
@@ -1,3 +1,53 @@
1
+ /**
2
+ * Client-side attachment-parser plugins.
3
+ *
4
+ * Some attachment formats can't be read by the model's web_fetch (binary) and
5
+ * have no server-side extractor either — e.g. legacy Hancom .hwp. A parser
6
+ * plugin runs IN THE BROWSER, turns the uploaded File into indexable text (or an
7
+ * HTML string), and the engine sends that content INLINE in the background
8
+ * indexing request — so the model indexes the parsed content directly, with no
9
+ * upload-side server extraction and no web_fetch for that file.
10
+ *
11
+ * Register a parser with `registerAttachmentParser()`, or via
12
+ * `configureChatEngine({ attachmentParsers: [...] })`. The BunnyQuery widget also
13
+ * exposes `BunnyQuery.registerAttachmentParser()` and an `attachmentParsers`
14
+ * init option. First matching parser wins.
15
+ */
16
+ interface AttachmentParser {
17
+ /** Human-readable label — used only in logs. */
18
+ name?: string;
19
+ /**
20
+ * Return true if this parser handles the file. Receives the file name and
21
+ * (when known) its MIME type. Keep it cheap — it runs for every upload.
22
+ */
23
+ match: (file: {
24
+ name: string;
25
+ mime?: string;
26
+ }) => boolean;
27
+ /**
28
+ * Parse the File into indexable plain text OR an HTML string (the model reads
29
+ * either). Runs in the browser; may be async. Return a falsy/empty value to
30
+ * skip (the file then falls back to web_fetch / server extraction).
31
+ */
32
+ parse: (file: File) => string | null | undefined | Promise<string | null | undefined>;
33
+ }
34
+ declare const MAX_PARSED_CONTENT_CHARS = 200000;
35
+ /** Register an attachment parser. Ignores duplicates (by reference) and invalid plugins. */
36
+ declare function registerAttachmentParser(parser: AttachmentParser): void;
37
+ /** Remove all registered parsers (mainly for tests / re-init). */
38
+ declare function clearAttachmentParsers(): void;
39
+ /** Snapshot of the registered parsers. */
40
+ declare function getAttachmentParsers(): AttachmentParser[];
41
+ /** First parser whose `match` returns true for the given file, if any. */
42
+ declare function findAttachmentParser(name: string, mime?: string): AttachmentParser | undefined;
43
+ /**
44
+ * Run the matching parser (if any) and return capped, trimmed content — or null
45
+ * when there is no parser, the parser throws, or it yields nothing. Never throws:
46
+ * a parser failure degrades to null so the upload still completes (the file then
47
+ * resolves via its normal path).
48
+ */
49
+ declare function parseAttachmentContent(file: File, name: string, mime?: string): Promise<string | null>;
50
+
1
51
  /**
2
52
  * Engine configuration / dependency injection.
3
53
  *
@@ -12,6 +62,7 @@
12
62
  * `poll: 0` to get the early ack + a manual `.poll()` handle (needed for queued-
13
63
  * send cancel). So the request builders include `poll` only when it is set.
14
64
  */
65
+
15
66
  interface ChatEngineConfig {
16
67
  /** skapi.clientSecretRequest, bound to the consumer's skapi instance. */
17
68
  clientSecretRequest: (opts: any) => Promise<any>;
@@ -24,6 +75,12 @@ interface ChatEngineConfig {
24
75
  * the `poll` key is omitted entirely (agent.vue). BunnyQuery sets `0`.
25
76
  */
26
77
  poll?: number;
78
+ /**
79
+ * Optional client-side attachment parsers (e.g. an .hwp parser). Each is
80
+ * registered at configure time; more can be added later via
81
+ * `registerAttachmentParser()`. See attachment_parsers.ts.
82
+ */
83
+ attachmentParsers?: AttachmentParser[];
27
84
  }
28
85
  declare function configureChatEngine(config: ChatEngineConfig): void;
29
86
  declare function chatEngineConfig(): ChatEngineConfig;
@@ -153,6 +210,12 @@ type BuildIndexingUserMessageOptions = {
153
210
  * drops the temporary-URL line — there is nothing for the model to fetch).
154
211
  */
155
212
  inlineContentPlaceholder?: string;
213
+ /**
214
+ * Actual file content parsed CLIENT-SIDE by an attachment-parser plugin (e.g.
215
+ * an .hwp parser). Embedded inline verbatim — no server extraction and no
216
+ * web_fetch for this file. Takes precedence over `inlineContentPlaceholder`.
217
+ */
218
+ inlineContent?: string;
156
219
  };
157
220
  declare function buildIndexingUserMessage(attachment: IndexingAttachmentInfo, options?: BuildIndexingUserMessageOptions): string;
158
221
 
@@ -293,6 +356,12 @@ type AttachmentSaveInfo = {
293
356
  size?: number;
294
357
  url: string;
295
358
  };
359
+ /**
360
+ * Content parsed CLIENT-SIDE by an attachment-parser plugin (e.g. an .hwp
361
+ * parser). When set, it is inlined into the indexing message verbatim and
362
+ * takes precedence over server-side office extraction / web_fetch.
363
+ */
364
+ parsedContent?: string;
296
365
  };
297
366
  declare function notifyAgentSaveAttachment(info: AttachmentSaveInfo): Promise<any>;
298
367
  declare function extractClaudeText(response: any): any;
@@ -475,6 +544,8 @@ declare class ChatSession {
475
544
  private typewriterQueue;
476
545
  enqueueTypewrite(idx: number, fullText: string, localId?: string): Promise<any>;
477
546
  typewriteLatestReply(key: string): Promise<any>;
547
+ _removeStrayPendingAssistants(): void;
548
+ _clearPendingUserBubble(itemId: string): void;
478
549
  resumePendingRequest(token: number): Promise<void>;
479
550
  handleHistoryItemResolution(itemId: string, response: any, platform: string): void;
480
551
  applyHistoryItemResolution(itemId: string, response: any, platform: string): void;
@@ -494,4 +565,4 @@ declare class ChatSession {
494
565
  bumpGate(): void;
495
566
  }
496
567
 
497
- export { type AttachmentFailureGroup, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
568
+ export { type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
package/dist/engine.d.ts CHANGED
@@ -1,3 +1,53 @@
1
+ /**
2
+ * Client-side attachment-parser plugins.
3
+ *
4
+ * Some attachment formats can't be read by the model's web_fetch (binary) and
5
+ * have no server-side extractor either — e.g. legacy Hancom .hwp. A parser
6
+ * plugin runs IN THE BROWSER, turns the uploaded File into indexable text (or an
7
+ * HTML string), and the engine sends that content INLINE in the background
8
+ * indexing request — so the model indexes the parsed content directly, with no
9
+ * upload-side server extraction and no web_fetch for that file.
10
+ *
11
+ * Register a parser with `registerAttachmentParser()`, or via
12
+ * `configureChatEngine({ attachmentParsers: [...] })`. The BunnyQuery widget also
13
+ * exposes `BunnyQuery.registerAttachmentParser()` and an `attachmentParsers`
14
+ * init option. First matching parser wins.
15
+ */
16
+ interface AttachmentParser {
17
+ /** Human-readable label — used only in logs. */
18
+ name?: string;
19
+ /**
20
+ * Return true if this parser handles the file. Receives the file name and
21
+ * (when known) its MIME type. Keep it cheap — it runs for every upload.
22
+ */
23
+ match: (file: {
24
+ name: string;
25
+ mime?: string;
26
+ }) => boolean;
27
+ /**
28
+ * Parse the File into indexable plain text OR an HTML string (the model reads
29
+ * either). Runs in the browser; may be async. Return a falsy/empty value to
30
+ * skip (the file then falls back to web_fetch / server extraction).
31
+ */
32
+ parse: (file: File) => string | null | undefined | Promise<string | null | undefined>;
33
+ }
34
+ declare const MAX_PARSED_CONTENT_CHARS = 200000;
35
+ /** Register an attachment parser. Ignores duplicates (by reference) and invalid plugins. */
36
+ declare function registerAttachmentParser(parser: AttachmentParser): void;
37
+ /** Remove all registered parsers (mainly for tests / re-init). */
38
+ declare function clearAttachmentParsers(): void;
39
+ /** Snapshot of the registered parsers. */
40
+ declare function getAttachmentParsers(): AttachmentParser[];
41
+ /** First parser whose `match` returns true for the given file, if any. */
42
+ declare function findAttachmentParser(name: string, mime?: string): AttachmentParser | undefined;
43
+ /**
44
+ * Run the matching parser (if any) and return capped, trimmed content — or null
45
+ * when there is no parser, the parser throws, or it yields nothing. Never throws:
46
+ * a parser failure degrades to null so the upload still completes (the file then
47
+ * resolves via its normal path).
48
+ */
49
+ declare function parseAttachmentContent(file: File, name: string, mime?: string): Promise<string | null>;
50
+
1
51
  /**
2
52
  * Engine configuration / dependency injection.
3
53
  *
@@ -12,6 +62,7 @@
12
62
  * `poll: 0` to get the early ack + a manual `.poll()` handle (needed for queued-
13
63
  * send cancel). So the request builders include `poll` only when it is set.
14
64
  */
65
+
15
66
  interface ChatEngineConfig {
16
67
  /** skapi.clientSecretRequest, bound to the consumer's skapi instance. */
17
68
  clientSecretRequest: (opts: any) => Promise<any>;
@@ -24,6 +75,12 @@ interface ChatEngineConfig {
24
75
  * the `poll` key is omitted entirely (agent.vue). BunnyQuery sets `0`.
25
76
  */
26
77
  poll?: number;
78
+ /**
79
+ * Optional client-side attachment parsers (e.g. an .hwp parser). Each is
80
+ * registered at configure time; more can be added later via
81
+ * `registerAttachmentParser()`. See attachment_parsers.ts.
82
+ */
83
+ attachmentParsers?: AttachmentParser[];
27
84
  }
28
85
  declare function configureChatEngine(config: ChatEngineConfig): void;
29
86
  declare function chatEngineConfig(): ChatEngineConfig;
@@ -153,6 +210,12 @@ type BuildIndexingUserMessageOptions = {
153
210
  * drops the temporary-URL line — there is nothing for the model to fetch).
154
211
  */
155
212
  inlineContentPlaceholder?: string;
213
+ /**
214
+ * Actual file content parsed CLIENT-SIDE by an attachment-parser plugin (e.g.
215
+ * an .hwp parser). Embedded inline verbatim — no server extraction and no
216
+ * web_fetch for this file. Takes precedence over `inlineContentPlaceholder`.
217
+ */
218
+ inlineContent?: string;
156
219
  };
157
220
  declare function buildIndexingUserMessage(attachment: IndexingAttachmentInfo, options?: BuildIndexingUserMessageOptions): string;
158
221
 
@@ -293,6 +356,12 @@ type AttachmentSaveInfo = {
293
356
  size?: number;
294
357
  url: string;
295
358
  };
359
+ /**
360
+ * Content parsed CLIENT-SIDE by an attachment-parser plugin (e.g. an .hwp
361
+ * parser). When set, it is inlined into the indexing message verbatim and
362
+ * takes precedence over server-side office extraction / web_fetch.
363
+ */
364
+ parsedContent?: string;
296
365
  };
297
366
  declare function notifyAgentSaveAttachment(info: AttachmentSaveInfo): Promise<any>;
298
367
  declare function extractClaudeText(response: any): any;
@@ -475,6 +544,8 @@ declare class ChatSession {
475
544
  private typewriterQueue;
476
545
  enqueueTypewrite(idx: number, fullText: string, localId?: string): Promise<any>;
477
546
  typewriteLatestReply(key: string): Promise<any>;
547
+ _removeStrayPendingAssistants(): void;
548
+ _clearPendingUserBubble(itemId: string): void;
478
549
  resumePendingRequest(token: number): Promise<void>;
479
550
  handleHistoryItemResolution(itemId: string, response: any, platform: string): void;
480
551
  applyHistoryItemResolution(itemId: string, response: any, platform: string): void;
@@ -494,4 +565,4 @@ declare class ChatSession {
494
565
  bumpGate(): void;
495
566
  }
496
567
 
497
- export { type AttachmentFailureGroup, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
568
+ export { type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
package/dist/engine.mjs CHANGED
@@ -1,7 +1,55 @@
1
+ // src/engine/attachment_parsers.ts
2
+ var MAX_PARSED_CONTENT_CHARS = 2e5;
3
+ var _parsers = [];
4
+ function registerAttachmentParser(parser) {
5
+ if (parser && typeof parser.match === "function" && typeof parser.parse === "function" && _parsers.indexOf(parser) === -1) {
6
+ _parsers.push(parser);
7
+ }
8
+ }
9
+ function clearAttachmentParsers() {
10
+ _parsers.length = 0;
11
+ }
12
+ function getAttachmentParsers() {
13
+ return _parsers.slice();
14
+ }
15
+ function findAttachmentParser(name, mime) {
16
+ for (let i = 0; i < _parsers.length; i++) {
17
+ try {
18
+ if (_parsers[i].match({ name, mime })) return _parsers[i];
19
+ } catch {
20
+ }
21
+ }
22
+ return void 0;
23
+ }
24
+ async function parseAttachmentContent(file, name, mime) {
25
+ const parser = findAttachmentParser(name, mime);
26
+ if (!parser) return null;
27
+ let raw;
28
+ try {
29
+ raw = await parser.parse(file);
30
+ } catch (err) {
31
+ console.error(
32
+ `[chat-engine] attachment parser ${parser.name || "(unnamed)"} failed for ${name}:`,
33
+ err
34
+ );
35
+ return null;
36
+ }
37
+ let text = (raw == null ? "" : String(raw)).trim();
38
+ if (!text) return null;
39
+ if (text.length > MAX_PARSED_CONTENT_CHARS) {
40
+ text = text.slice(0, MAX_PARSED_CONTENT_CHARS) + `
41
+ ...[truncated for length; original ${text.length} characters]`;
42
+ }
43
+ return text;
44
+ }
45
+
1
46
  // src/engine/config.ts
2
47
  var _config = null;
3
48
  function configureChatEngine(config) {
4
49
  _config = config;
50
+ if (config.attachmentParsers) {
51
+ for (const parser of config.attachmentParsers) registerAttachmentParser(parser);
52
+ }
5
53
  }
6
54
  function chatEngineConfig() {
7
55
  if (!_config) {
@@ -28,13 +76,41 @@ var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
28
76
  "pptx",
29
77
  "pptm",
30
78
  "hwp",
31
- "hwpx"
79
+ "hwpx",
80
+ "ods",
81
+ "odt",
82
+ "odp",
83
+ "epub"
84
+ ]);
85
+ var WEB_FETCHABLE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
86
+ "csv",
87
+ "tsv",
88
+ "tab",
89
+ "txt",
90
+ "text",
91
+ "md",
92
+ "markdown",
93
+ "json",
94
+ "ndjson",
95
+ "jsonl",
96
+ "xml",
97
+ "yaml",
98
+ "yml",
99
+ "log",
100
+ // RTF is a TEXT format (not a binary zip), so web_fetch can read it and the
101
+ // model decodes its control words. Pin it here so a `.rtf` reported as
102
+ // `application/msword` isn't misrouted to server-side extraction (which has no
103
+ // .rtf extractor → "unsupported format" note).
104
+ "rtf",
105
+ "htm",
106
+ "html"
32
107
  ]);
33
108
  function isOfficeFile(name, mime) {
34
109
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
35
110
  if (OFFICE_FILE_EXTENSIONS.has(ext)) return true;
111
+ if (WEB_FETCHABLE_TEXT_EXTENSIONS.has(ext)) return false;
36
112
  const m = (mime || "").toLowerCase();
37
- return m.includes("officedocument") || m.includes("hwp") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
113
+ return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
38
114
  }
39
115
  var _extractPlaceholderSeq = 0;
40
116
  function makeExtractPlaceholder(seed) {
@@ -155,6 +231,14 @@ File metadata:
155
231
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
156
232
  ` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
157
233
  ` : "");
234
+ if (options?.inlineContent) {
235
+ return head + `
236
+ The file's content was parsed by the client and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
237
+
238
+ ----- BEGIN FILE CONTENT -----
239
+ ${options.inlineContent}
240
+ ----- END FILE CONTENT -----`;
241
+ }
158
242
  if (options?.inlineContentPlaceholder) {
159
243
  return head + `
160
244
  The file's text content was extracted on the server and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
@@ -653,14 +737,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
653
737
  });
654
738
  }
655
739
  async function notifyAgentSaveAttachment(info) {
656
- const { platform, service, owner, attachment } = info;
657
- const office = isOfficeFile(attachment.name, attachment.mime);
740
+ const { platform, service, owner, attachment, parsedContent } = info;
741
+ const office = !parsedContent && isOfficeFile(attachment.name, attachment.mime);
658
742
  const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : void 0;
659
743
  const extractContent = office && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
660
744
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
661
745
  const userMessage = buildIndexingUserMessage(
662
746
  attachment,
663
- placeholder ? { inlineContentPlaceholder: placeholder } : void 0
747
+ parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : void 0
664
748
  );
665
749
  const systemPrompt = buildIndexingSystemPrompt({
666
750
  service,
@@ -1246,6 +1330,7 @@ var ChatSession = class {
1246
1330
  this.enqueueTypewrite(aiIdx, answer, lid);
1247
1331
  }
1248
1332
  }
1333
+ this._removeStrayPendingAssistants();
1249
1334
  this.promoteNextQueuedToRunning();
1250
1335
  this.updateHistoryCache();
1251
1336
  this.host.notify();
@@ -1288,6 +1373,7 @@ var ChatSession = class {
1288
1373
  if (thPos2 !== -1) this.state.messages.splice(thPos2, 1);
1289
1374
  }
1290
1375
  if (serverId) this.cancelledServerIds.delete(serverId);
1376
+ this._removeStrayPendingAssistants();
1291
1377
  this.promoteNextQueuedToRunning();
1292
1378
  this.updateHistoryCache();
1293
1379
  this.host.notify();
@@ -1301,6 +1387,7 @@ var ChatSession = class {
1301
1387
  return;
1302
1388
  }
1303
1389
  this.insertAtTarget({ role: "assistant", content: getErrorMessage(err), isError: true }, targetIdx);
1390
+ this._removeStrayPendingAssistants();
1304
1391
  this.promoteNextQueuedToRunning();
1305
1392
  this.updateHistoryCache();
1306
1393
  this.host.notify();
@@ -1462,16 +1549,48 @@ var ChatSession = class {
1462
1549
  if (pendingIdx === -1) return Promise.resolve();
1463
1550
  if (latest.isError || !latest.content) {
1464
1551
  this.state.messages[pendingIdx] = { role: "assistant", content: latest.content || "", isError: !!latest.isError };
1552
+ this._removeStrayPendingAssistants();
1465
1553
  this.host.notify();
1466
1554
  this.promoteNextQueuedToRunning();
1467
1555
  return Promise.resolve();
1468
1556
  }
1469
1557
  var lid = this._newLocalId();
1470
1558
  this.state.messages[pendingIdx] = { role: "assistant", content: "", isPending: false, _localId: lid };
1559
+ this._removeStrayPendingAssistants();
1471
1560
  this.host.notify();
1472
1561
  this.promoteNextQueuedToRunning();
1473
1562
  return this.enqueueTypewrite(pendingIdx, latest.content, lid);
1474
1563
  }
1564
+ // Remove any leftover non-background pending ("Thinking…") assistant bubbles.
1565
+ // There is normally at most ONE such bubble at a time (promoteNext* refuses to
1566
+ // add a second), so any extra is a duplicate — it appears when a concurrent
1567
+ // history refetch re-maps the still-"running" turn into a pending placeholder
1568
+ // (with a real _serverItemId) while the local pending bubble (no _serverItemId)
1569
+ // is rescued and re-appended (see loadHistory rescue below). Each resolve path
1570
+ // only replaces the FIRST pending bubble, so without this a stray "Thinking…"
1571
+ // survives next to the reply/error. MUST run AFTER the resolved bubble has been
1572
+ // made non-pending and BEFORE promoteNext*() (so a freshly-promoted Thinking,
1573
+ // which is added only once no pending assistant remains, is preserved).
1574
+ _removeStrayPendingAssistants() {
1575
+ for (var k = this.state.messages.length - 1; k >= 0; k--) {
1576
+ var m = this.state.messages[k];
1577
+ if (m.isPending && m.role === "assistant" && !m.isBackgroundTask) this.state.messages.splice(k, 1);
1578
+ }
1579
+ }
1580
+ // Drop the pending flags on the resolved turn's USER bubble (preserving its
1581
+ // content + background-task marker). Needed because a bg "Indexing:" turn's user
1582
+ // bubble carries isPendingInProcess; leaving it set keeps the bubble visually
1583
+ // stuck and keeps its bgTaskQueue entry alive forever.
1584
+ _clearPendingUserBubble(itemId) {
1585
+ var uIdx = this.state.messages.findIndex(function(m) {
1586
+ return m.role === "user" && m._serverItemId === itemId && (m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
1587
+ });
1588
+ if (uIdx === -1) return;
1589
+ var u = this.state.messages[uIdx];
1590
+ var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
1591
+ if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
1592
+ this.state.messages[uIdx] = cleaned;
1593
+ }
1475
1594
  // If an immediate-send request for the current cache key is still in flight
1476
1595
  // (e.g. the view unmounted then remounted mid-request), show the sending
1477
1596
  // state, await it, then render the reply from the cache. Skipped when the
@@ -1510,6 +1629,7 @@ var ChatSession = class {
1510
1629
  return m.isPending && m._serverItemId === itemId;
1511
1630
  });
1512
1631
  if (idx !== -1) {
1632
+ this._clearPendingUserBubble(itemId);
1513
1633
  if (isErr) {
1514
1634
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
1515
1635
  this.host.notify();
@@ -1659,12 +1779,16 @@ var ChatSession = class {
1659
1779
  self.state.messages.forEach(function(m) {
1660
1780
  if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
1661
1781
  });
1782
+ var mappedHasPendingAssistant = mapped.some(function(m) {
1783
+ return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
1784
+ });
1662
1785
  var rescued = [];
1663
1786
  for (var ri = 0; ri < self.state.messages.length; ri++) {
1664
1787
  var mm = self.state.messages[ri];
1665
1788
  if (mm.isBackgroundTask) continue;
1666
1789
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
1667
1790
  if (!mm._serverItemId) {
1791
+ if (mappedHasPendingAssistant) continue;
1668
1792
  if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
1669
1793
  else if (self.state.sending && mm.role === "user") {
1670
1794
  var next = self.state.messages[ri + 1];
@@ -1822,44 +1946,49 @@ var ChatSession = class {
1822
1946
  att.storagePath = member.storagePath;
1823
1947
  }
1824
1948
  var mime = member.file.type || self.host.getMimeType(member.file.name);
1825
- return notifyAgentSaveAttachment({
1826
- platform: id.platform,
1827
- model: id.model,
1828
- service: id.serviceId,
1829
- owner: id.owner,
1830
- userId: id.userId || id.serviceId,
1831
- serviceName: id.serviceName,
1832
- serviceDescription: id.serviceDescription,
1833
- attachment: {
1834
- name: member.file.name,
1835
- storagePath: member.storagePath,
1836
- mime: mime || void 0,
1837
- size: member.file.size,
1838
- url
1839
- }
1840
- }).then(function(ack) {
1841
- if (ack && typeof ack.id === "string") {
1842
- self.bgTaskQueue.push({
1843
- serviceId: id.serviceId,
1844
- platform: id.platform,
1845
- id: ack.id,
1846
- filename: member.file.name,
1949
+ return Promise.resolve(
1950
+ parseAttachmentContent(member.file, member.file.name, mime || void 0)
1951
+ ).then(function(parsedContent) {
1952
+ return notifyAgentSaveAttachment({
1953
+ platform: id.platform,
1954
+ model: id.model,
1955
+ service: id.serviceId,
1956
+ owner: id.owner,
1957
+ userId: id.userId || id.serviceId,
1958
+ serviceName: id.serviceName,
1959
+ serviceDescription: id.serviceDescription,
1960
+ attachment: {
1961
+ name: member.file.name,
1847
1962
  storagePath: member.storagePath,
1848
- isReindex: hadExists,
1849
1963
  mime: mime || void 0,
1850
1964
  size: member.file.size,
1851
- status: ack.status === "running" ? "running" : "pending",
1852
- poll: ack.poll
1853
- });
1854
- self.drainBgTaskQueue();
1855
- }
1856
- }, function(e) {
1857
- console.error("[chat-engine] indexing request failed", e);
1858
- anyIndexFailed = true;
1859
- if (!att.errorCode && !att.errorDetail) {
1860
- att.errorCode = e && (e.code || e.body && e.body.code) || "";
1861
- att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
1862
- }
1965
+ url
1966
+ },
1967
+ parsedContent: parsedContent || void 0
1968
+ }).then(function(ack) {
1969
+ if (ack && typeof ack.id === "string") {
1970
+ self.bgTaskQueue.push({
1971
+ serviceId: id.serviceId,
1972
+ platform: id.platform,
1973
+ id: ack.id,
1974
+ filename: member.file.name,
1975
+ storagePath: member.storagePath,
1976
+ isReindex: hadExists,
1977
+ mime: mime || void 0,
1978
+ size: member.file.size,
1979
+ status: ack.status === "running" ? "running" : "pending",
1980
+ poll: ack.poll
1981
+ });
1982
+ self.drainBgTaskQueue();
1983
+ }
1984
+ }, function(e) {
1985
+ console.error("[chat-engine] indexing request failed", e);
1986
+ anyIndexFailed = true;
1987
+ if (!att.errorCode && !att.errorDetail) {
1988
+ att.errorCode = e && (e.code || e.body && e.body.code) || "";
1989
+ att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
1990
+ }
1991
+ });
1863
1992
  });
1864
1993
  });
1865
1994
  });
@@ -1939,6 +2068,6 @@ var ChatSession = class {
1939
2068
  }
1940
2069
  };
1941
2070
 
1942
- export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
2071
+ export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
1943
2072
  //# sourceMappingURL=engine.mjs.map
1944
2073
  //# sourceMappingURL=engine.mjs.map