@hachej/boring-agent 0.1.31 → 0.1.33

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DebugDrawer
3
- } from "./chunk-MMJA3QON.js";
3
+ } from "./chunk-UTUXVT2K.js";
4
4
  export {
5
5
  DebugDrawer
6
6
  };
@@ -0,0 +1,85 @@
1
+ // src/shared/message-sanitizer.ts
2
+ function collapseExactRepeatedText(text) {
3
+ const maxCopies = Math.min(20, Math.floor(text.length / 4));
4
+ for (let copies = maxCopies; copies >= 2; copies -= 1) {
5
+ if (text.length % copies !== 0) continue;
6
+ const unit = text.slice(0, text.length / copies);
7
+ if (unit.length < 4) continue;
8
+ if (unit.repeat(copies) === text) return unit;
9
+ }
10
+ return text;
11
+ }
12
+ function textFromMessage(message, role) {
13
+ if (message.role !== role) return null;
14
+ const text = (message.parts ?? []).map((part) => {
15
+ const candidate = part;
16
+ return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
17
+ }).join("").trim();
18
+ return text || null;
19
+ }
20
+ function assistantVisibleText(message) {
21
+ return textFromMessage(message, "assistant");
22
+ }
23
+ function userVisibleText(message) {
24
+ return textFromMessage(message, "user");
25
+ }
26
+ function isTransientUserId(id) {
27
+ return /^user-\d+$/.test(id);
28
+ }
29
+ function isEmptyAssistantMessage(message) {
30
+ return message.role === "assistant" && (!message.parts || message.parts.length === 0);
31
+ }
32
+ function sanitizeUiMessage(message) {
33
+ if (message.role !== "assistant") return message;
34
+ let changed = false;
35
+ const parts = [];
36
+ for (const part of message.parts ?? []) {
37
+ const candidate = part;
38
+ if (candidate.type !== "text" || typeof candidate.text !== "string") {
39
+ parts.push(part);
40
+ continue;
41
+ }
42
+ const text = collapseExactRepeatedText(candidate.text);
43
+ const previous = parts[parts.length - 1];
44
+ if (previous?.type === "text" && previous.text === text) {
45
+ changed = true;
46
+ continue;
47
+ }
48
+ if (text !== candidate.text) changed = true;
49
+ parts.push(text === candidate.text ? part : { ...candidate, text });
50
+ }
51
+ return changed ? { ...message, parts } : message;
52
+ }
53
+ function uiMessageContentKey(message) {
54
+ const sanitized = sanitizeUiMessage(message);
55
+ return `${sanitized.role}:${JSON.stringify(sanitized.parts ?? [])}`;
56
+ }
57
+ function dropEmptyAssistantUiMessages(messages) {
58
+ return messages.filter((message) => !isEmptyAssistantMessage(message));
59
+ }
60
+ function sanitizeUiMessages(messages, options = {}) {
61
+ const deduped = [];
62
+ const seenUserText = /* @__PURE__ */ new Set();
63
+ for (const rawMessage of messages) {
64
+ if (options.dropEmptyAssistantMessages && isEmptyAssistantMessage(rawMessage)) continue;
65
+ const message = sanitizeUiMessage(rawMessage);
66
+ const userText = userVisibleText(message);
67
+ const id = typeof message.id === "string" ? message.id : "";
68
+ if (userText) {
69
+ if (seenUserText.has(userText) && isTransientUserId(id)) continue;
70
+ if (!id.startsWith("pending-user:")) seenUserText.add(userText);
71
+ }
72
+ const previous = deduped[deduped.length - 1];
73
+ const assistantText = assistantVisibleText(message);
74
+ if (assistantText && previous && assistantVisibleText(previous) === assistantText) continue;
75
+ deduped.push(message);
76
+ }
77
+ return deduped;
78
+ }
79
+
80
+ export {
81
+ sanitizeUiMessage,
82
+ uiMessageContentKey,
83
+ dropEmptyAssistantUiMessages,
84
+ sanitizeUiMessages
85
+ };
@@ -1,6 +1,36 @@
1
1
  // src/front/DebugDrawer.tsx
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
 
4
+ // src/front/clipboard.ts
5
+ async function copyTextToClipboard(text) {
6
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
7
+ try {
8
+ await navigator.clipboard.writeText(text);
9
+ return true;
10
+ } catch {
11
+ }
12
+ }
13
+ if (typeof document === "undefined") return false;
14
+ const textarea = document.createElement("textarea");
15
+ textarea.value = text;
16
+ textarea.setAttribute("readonly", "");
17
+ textarea.style.position = "fixed";
18
+ textarea.style.top = "-9999px";
19
+ textarea.style.left = "-9999px";
20
+ textarea.style.opacity = "0";
21
+ textarea.style.pointerEvents = "none";
22
+ document.body.appendChild(textarea);
23
+ try {
24
+ textarea.focus();
25
+ textarea.select();
26
+ return document.execCommand?.("copy") ?? false;
27
+ } catch {
28
+ return false;
29
+ } finally {
30
+ document.body.removeChild(textarea);
31
+ }
32
+ }
33
+
4
34
  // src/front/lib/index.ts
5
35
  import { clsx } from "clsx";
6
36
  import { twMerge } from "tailwind-merge";
@@ -21,9 +51,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
51
  function CopyButton({ value, label }) {
22
52
  const [copied, setCopied] = useState(false);
23
53
  const onCopy = useCallback(() => {
24
- const writeText = navigator.clipboard?.writeText?.bind(navigator.clipboard);
25
- if (!writeText) return;
26
- void writeText(value).then(() => {
54
+ void copyTextToClipboard(value).then((ok) => {
55
+ if (!ok) return;
27
56
  setCopied(true);
28
57
  window.setTimeout(() => setCopied(false), 1200);
29
58
  });
@@ -291,5 +320,6 @@ function DebugDrawer({ sessionId, messages, requestHeaders, width, onWidthChange
291
320
 
292
321
  export {
293
322
  cn,
323
+ copyTextToClipboard,
294
324
  DebugDrawer
295
325
  };
@@ -4,8 +4,7 @@ import { FileUIPart, UIMessage, ChatStatus } from 'ai';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent } from 'react';
6
6
  import { T as ToolUiMetadata } from '../tool-ui-DIFNGwYd.js';
7
- import * as _ai_sdk_react from '@ai-sdk/react';
8
- import { S as SendMessageInput, e as SessionSummary } from '../harness-CQ0uw6xW.js';
7
+ import { S as SendMessageInput, e as SessionSummary } from '../harness-dYuMzxmp.js';
9
8
  import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupAddon, InputGroupButton, InputGroupTextarea } from '@hachej/boring-ui-kit';
10
9
  import { Streamdown } from 'streamdown';
11
10
  import { StickToBottom } from 'use-stick-to-bottom';
@@ -165,16 +164,24 @@ type ComposerBlocker = {
165
164
  sessionId?: string;
166
165
  actions?: ComposerBlockerAction[];
167
166
  };
167
+ type ChatPanelRuntimeDependenciesWarmupStatus = {
168
+ state: 'preparing' | 'ready' | 'failed';
169
+ message?: string;
170
+ requirement?: string;
171
+ };
168
172
  type ChatPanelWorkspaceWarmupStatus = {
169
173
  status: 'preparing';
170
174
  requirement?: 'workspace-fs' | 'sandbox-exec' | 'ui-bridge';
171
175
  message?: string;
176
+ runtimeDependencies?: ChatPanelRuntimeDependenciesWarmupStatus;
172
177
  } | {
173
178
  status: 'ready';
179
+ runtimeDependencies?: ChatPanelRuntimeDependenciesWarmupStatus;
174
180
  } | {
175
181
  status: 'failed';
176
182
  requirement?: 'workspace-fs' | 'sandbox-exec' | 'ui-bridge';
177
183
  message?: string;
184
+ runtimeDependencies?: ChatPanelRuntimeDependenciesWarmupStatus;
178
185
  };
179
186
  type ChatSubmitSource = 'composer' | 'suggestion';
180
187
  interface ChatSubmitContext {
@@ -320,25 +327,65 @@ type UseAgentChatOptions = Pick<SendMessageInput, 'sessionId' | 'model' | 'think
320
327
  persistMessages?: boolean;
321
328
  hydrateMessages?: boolean;
322
329
  };
323
- declare function useAgentChat(opts: UseAgentChatOptions): _ai_sdk_react.UseChatHelpers<UIMessage<unknown, ai.UIDataTypes, ai.UITools>>;
330
+ declare function useAgentChat(opts: UseAgentChatOptions): {
331
+ messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[];
332
+ sendMessage: (message?: (Omit<UIMessage<unknown, ai.UIDataTypes, ai.UITools>, "role" | "id"> & {
333
+ id?: string | undefined;
334
+ role?: "system" | "user" | "assistant" | undefined;
335
+ } & {
336
+ text?: never;
337
+ files?: never;
338
+ messageId?: string;
339
+ }) | {
340
+ text: string;
341
+ files?: FileList | ai.FileUIPart[];
342
+ metadata?: unknown;
343
+ parts?: never;
344
+ messageId?: string;
345
+ } | {
346
+ files: FileList | ai.FileUIPart[];
347
+ metadata?: unknown;
348
+ parts?: never;
349
+ messageId?: string;
350
+ } | undefined, options?: ai.ChatRequestOptions | undefined) => Promise<void>;
351
+ stop: () => void;
352
+ status: ai.ChatStatus;
353
+ hydrated: boolean;
354
+ hydratingMessages: boolean;
355
+ id: string;
356
+ setMessages: (messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[] | ((messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[]) => UIMessage<unknown, ai.UIDataTypes, ai.UITools>[])) => void;
357
+ error: Error | undefined;
358
+ regenerate: ({ messageId, ...options }?: {
359
+ messageId?: string;
360
+ } & ai.ChatRequestOptions) => Promise<void>;
361
+ resumeStream: (options?: ai.ChatRequestOptions) => Promise<void>;
362
+ addToolResult: ai.ChatAddToolOutputFunction<UIMessage<unknown, ai.UIDataTypes, ai.UITools>>;
363
+ addToolOutput: ai.ChatAddToolOutputFunction<UIMessage<unknown, ai.UIDataTypes, ai.UITools>>;
364
+ addToolApprovalResponse: ai.ChatAddToolApproveResponseFunction;
365
+ clearError: () => void;
366
+ };
324
367
 
325
368
  interface UseSessionsOptions {
326
369
  requestHeaders?: Record<string, string>;
327
370
  storageKey?: string;
328
371
  enabled?: boolean;
329
372
  refreshKey?: unknown;
373
+ initialActiveSessionId?: string;
330
374
  }
331
375
  interface UseSessionsResult {
332
376
  sessions: SessionSummary[];
333
377
  activeSession: SessionSummary | undefined;
334
378
  activeSessionId: string | undefined;
335
379
  loading: boolean;
380
+ loadingMore: boolean;
381
+ hasMore: boolean;
336
382
  error: Error | undefined;
337
383
  create: (init?: {
338
384
  title?: string;
339
385
  }) => Promise<SessionSummary>;
340
386
  switch: (id: string) => void;
341
387
  delete: (id: string) => Promise<void>;
388
+ loadMore: () => Promise<void>;
342
389
  }
343
390
  declare function useSessions(opts?: UseSessionsOptions): UseSessionsResult;
344
391