@hachej/boring-agent 0.1.58 → 0.1.60

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.
@@ -523,6 +523,51 @@ interface SessionListProps {
523
523
  declare function SessionList({ sessions, activeId, loading, onSwitch, onCreate, onDelete, onLoadMore, hasMore, loadingMore, onClose, className, }: SessionListProps): react_jsx_runtime.JSX.Element;
524
524
  declare const SessionBrowser: typeof SessionList;
525
525
 
526
+ interface PiSessionSearchItem {
527
+ id: string;
528
+ /** boring-ui session title maps to pi's session display name. */
529
+ title?: string | null;
530
+ /** Optional pi-style session name for callers that already expose it. */
531
+ name?: string | null;
532
+ /** Optional full transcript search text when available from a pi session listing. */
533
+ allMessagesText?: string | null;
534
+ /** Optional cwd from the underlying pi session. */
535
+ cwd?: string | null;
536
+ updatedAt?: string | number | Date | null;
537
+ }
538
+ type PiSessionSearchSortMode = "fuzzy" | "recent";
539
+ interface PiSessionSearchOptions {
540
+ sortMode?: PiSessionSearchSortMode;
541
+ limit?: number;
542
+ }
543
+ type SearchToken = {
544
+ kind: "fuzzy" | "phrase";
545
+ value: string;
546
+ };
547
+ type ParsedSearchQuery = {
548
+ mode: "tokens";
549
+ tokens: SearchToken[];
550
+ regex: null;
551
+ error?: undefined;
552
+ } | {
553
+ mode: "regex";
554
+ tokens: [];
555
+ regex: RegExp | null;
556
+ error?: string;
557
+ };
558
+ /**
559
+ * Parse session-search queries with the same user-facing syntax as pi's native
560
+ * session picker: whitespace fuzzy tokens, quoted exact phrases, and `re:`
561
+ * regex mode. Kept intentionally small so boring-agent frontends can reuse the
562
+ * pi search behavior without importing the terminal-only session picker UI.
563
+ */
564
+ declare function parsePiSessionSearchQuery(query: string): ParsedSearchQuery;
565
+ declare function matchPiSessionSearch(session: PiSessionSearchItem, parsed: ParsedSearchQuery): {
566
+ matches: boolean;
567
+ score: number;
568
+ };
569
+ declare function searchPiSessions<T extends PiSessionSearchItem>(sessions: readonly T[], query: string, options?: PiSessionSearchOptions): T[];
570
+
526
571
  interface ComposerBlockerAction {
527
572
  id: string;
528
573
  label: string;
@@ -592,6 +637,7 @@ interface PiChatPanelProps<TComposerBlocker extends ComposerBlocker = ComposerBl
592
637
  createRemoteSession?: (options: RemotePiSessionOptions) => RemotePiSession;
593
638
  remoteSessionOptions?: UsePiSessionsOptions['remoteSessionOptions'];
594
639
  hydrateMessages?: boolean;
640
+ allowPromptDuringInitialHydration?: boolean;
595
641
  workspaceWarmupStatus?: ChatPanelWorkspaceWarmupStatus;
596
642
  onSessionReset?: () => void | Promise<void>;
597
643
  onBeforeSubmit?: (draft: string, context: ChatSubmitContext) => false | void | boolean | Promise<false | void | boolean>;
@@ -599,6 +645,10 @@ interface PiChatPanelProps<TComposerBlocker extends ComposerBlocker = ComposerBl
599
645
  onCommandResult?: (message: string) => void;
600
646
  onComposerWarning?: (message: string) => void;
601
647
  onMentionedFilesConsumed?: () => void;
648
+ onPromptSubmitStarted?: (context: {
649
+ sessionId: string;
650
+ clientNonce: string;
651
+ }) => void;
602
652
  onData?: (part: unknown) => void;
603
653
  onOpenArtifact?: (path: string) => void;
604
654
  composerBlockers?: TComposerBlocker[];
@@ -613,7 +663,7 @@ interface PiChatPanelProps<TComposerBlocker extends ComposerBlocker = ComposerBl
613
663
  * knowing what the code means or what the action does. */
614
664
  renderNoticeAction?: (notice: PiChatRuntimeNotice) => ReactNode;
615
665
  }
616
- declare function PiChatPanel<TComposerBlocker extends ComposerBlocker = ComposerBlocker>({ sessionId, extraCommands, apiBaseUrl, workspaceId, storageScope, requestHeaders, storage, fetch, className, chrome, debug, showSessions, hotReloadEnabled, suggestions, emptyState, emptyPlacement, composerPlaceholder, initialDraft, autoSubmitInitialDraft, onDraftRestored, onAutoSubmitInitialDraftAccepted, onAutoSubmitInitialDraftSettled, model, defaultModel, availableModels, hideDefaultModelOption, hideComposerSettings, suppressPreSubmitCancelledWarning, thinkingLevel, thinkingControl, serverResourcesEnabled, mentionedFiles, commands, excludeBuiltinCommands, toolRenderers, createRemoteSession, remoteSessionOptions, hydrateMessages, workspaceWarmupStatus, onSessionReset, onBeforeSubmit, onReloadAgentPlugins, onCommandResult, onComposerWarning, onMentionedFilesConsumed, onData, onOpenArtifact, composerBlockers, onComposerStop, onComposerBlockerAction, onTurnComplete, renderNoticeAction, }: PiChatPanelProps<TComposerBlocker>): react_jsx_runtime.JSX.Element;
666
+ declare function PiChatPanel<TComposerBlocker extends ComposerBlocker = ComposerBlocker>({ sessionId, extraCommands, apiBaseUrl, workspaceId, storageScope, requestHeaders, storage, fetch, className, chrome, debug, showSessions, hotReloadEnabled, suggestions, emptyState, emptyPlacement, composerPlaceholder, initialDraft, autoSubmitInitialDraft, onDraftRestored, onAutoSubmitInitialDraftAccepted, onAutoSubmitInitialDraftSettled, model, defaultModel, availableModels, hideDefaultModelOption, hideComposerSettings, suppressPreSubmitCancelledWarning, thinkingLevel, thinkingControl, serverResourcesEnabled, mentionedFiles, commands, excludeBuiltinCommands, toolRenderers, createRemoteSession, remoteSessionOptions, hydrateMessages, allowPromptDuringInitialHydration, workspaceWarmupStatus, onSessionReset, onBeforeSubmit, onReloadAgentPlugins, onCommandResult, onComposerWarning, onMentionedFilesConsumed, onPromptSubmitStarted, onData, onOpenArtifact, composerBlockers, onComposerStop, onComposerBlockerAction, onTurnComplete, renderNoticeAction, }: PiChatPanelProps<TComposerBlocker>): react_jsx_runtime.JSX.Element;
617
667
 
618
668
  interface DebugDrawerProps {
619
669
  apiBaseUrl?: string;
@@ -716,4 +766,4 @@ declare const CodeBlock: ({ code, language, showLineNumbers, className, children
716
766
 
717
767
  declare function cn(...inputs: ClassValue[]): string;
718
768
 
719
- export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, PiChatPanel as ChatPanel, type PiChatPanelProps as ChatPanelProps, type ChatSuggestion, CodeBlock, type CommandRegistry, type ComposerBlocker, type ComposerBlockerAction, Conversation, ConversationContent, ConversationScrollButton, DebugDrawer, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, type OpenArtifactHandler, type ParsedCommand, PiChatPanel, type PiChatPanelProps, SessionBrowser as PiSessionBrowser, type PiSessionCreateInit, SessionList as PiSessionList, PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, type SessionListProps, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UsePiSessionsOptions, type UsePiSessionsResult, activeSessionStorageKey, builtinCommands, clearActiveSessionId, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, mergeShadcnToolRenderers, mergeToolRenderers, parseSlashCommand, readActiveSessionId, resolveToolRenderer, uploadFile, useOpenArtifact, usePiSessions, writeActiveSessionId };
769
+ export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, PiChatPanel as ChatPanel, type PiChatPanelProps as ChatPanelProps, type ChatSuggestion, CodeBlock, type CommandRegistry, type ComposerBlocker, type ComposerBlockerAction, Conversation, ConversationContent, ConversationScrollButton, DebugDrawer, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, type OpenArtifactHandler, type ParsedCommand, PiChatPanel, type PiChatPanelProps, SessionBrowser as PiSessionBrowser, type PiSessionCreateInit, SessionList as PiSessionList, type PiSessionSearchItem, type PiSessionSearchOptions, type PiSessionSearchSortMode, PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, type SessionListProps, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UsePiSessionsOptions, type UsePiSessionsResult, activeSessionStorageKey, builtinCommands, clearActiveSessionId, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, matchPiSessionSearch, mergeShadcnToolRenderers, mergeToolRenderers, parsePiSessionSearchQuery, parseSlashCommand, readActiveSessionId, resolveToolRenderer, searchPiSessions, uploadFile, useOpenArtifact, usePiSessions, writeActiveSessionId };
@@ -2309,7 +2309,10 @@ var PiFollowUpQueueController = class {
2309
2309
  return this.block("empty", "Enter a message before sending.");
2310
2310
  }
2311
2311
  const state = this.session.getState();
2312
- if (state.status === "hydrating") {
2312
+ if (state.status === "hydrating" && !(this.options.allowPromptDuringInitialHydration === true && canPromptDuringInitialHydration(state))) {
2313
+ return this.block("hydrating", "The agent session is still hydrating.");
2314
+ }
2315
+ if (state.status === "idle" && hasPendingOptimisticPromptInEmptySession(state)) {
2313
2316
  return this.block("hydrating", "The agent session is still hydrating.");
2314
2317
  }
2315
2318
  if (isPiChatBusy(state.status)) {
@@ -2379,6 +2382,12 @@ function createPiFollowUpQueueController(session, options) {
2379
2382
  function isPiChatBusy(status) {
2380
2383
  return status === "submitted" || status === "streaming" || status === "aborting";
2381
2384
  }
2385
+ function canPromptDuringInitialHydration(state) {
2386
+ return !state.hydrated && state.history.messageCount === 0 && state.committedMessages.length === 0 && state.queue.followUps.length === 0 && Object.keys(state.optimisticOutbox).length === 0 && !state.streamingMessage;
2387
+ }
2388
+ function hasPendingOptimisticPromptInEmptySession(state) {
2389
+ return state.hydrated && state.history.messageCount === 0 && state.committedMessages.length === 0 && Object.values(state.optimisticOutbox).some((message) => message.clientSeq === void 0);
2390
+ }
2382
2391
  function nextFollowUpClientSeq(state, floor = 1) {
2383
2392
  let maxSeq = floor - 1;
2384
2393
  for (const queued of state.queue.followUps) {
@@ -3776,13 +3785,15 @@ function hydrateFromSnapshot(state, snapshot, options = {}) {
3776
3785
  for (const followUp of queue.followUps) {
3777
3786
  if (followUp.clientNonce) serverNonces.add(followUp.clientNonce);
3778
3787
  }
3788
+ const preserveInitialOptimisticOutbox = shouldPreserveInitialOptimisticOutbox(state, snapshot, snapshotMessages);
3789
+ const preserveOutbox = preserveLocalHistory || preserveInitialOptimisticOutbox;
3779
3790
  const nextOutbox = {};
3780
- if (preserveLocalHistory) {
3791
+ if (preserveOutbox) {
3781
3792
  for (const message of Object.values(state.optimisticOutbox)) {
3782
3793
  if (!serverNonces.has(message.clientNonce)) nextOutbox[message.clientNonce] = message;
3783
3794
  }
3784
3795
  }
3785
- const staleOutbox = preserveLocalHistory ? [] : Object.values(state.optimisticOutbox).filter((message) => !serverNonces.has(message.clientNonce));
3796
+ const staleOutbox = preserveOutbox ? [] : Object.values(state.optimisticOutbox).filter((message) => !serverNonces.has(message.clientNonce));
3786
3797
  const notices = staleOutbox.length > 0 ? upsertNotice(state.notices, {
3787
3798
  id: "stale-outbox-cleared",
3788
3799
  level: "warning",
@@ -3809,6 +3820,9 @@ function hydrateFromSnapshot(state, snapshot, options = {}) {
3809
3820
  needsResync: void 0
3810
3821
  };
3811
3822
  }
3823
+ function shouldPreserveInitialOptimisticOutbox(state, snapshot, snapshotMessages) {
3824
+ return !state.hydrated && Object.keys(state.optimisticOutbox).length > 0 && snapshotMessages.length === 0 && snapshot.queue.followUps.length === 0;
3825
+ }
3812
3826
  function shouldPreserveLocalCommittedHistory(state, snapshot, snapshotMessages, options) {
3813
3827
  if (options.allowSeqRewind) return false;
3814
3828
  if (!state.hydrated) return false;
@@ -5649,6 +5663,190 @@ function sortByUpdatedDesc(a, b) {
5649
5663
  return a.id.localeCompare(b.id);
5650
5664
  }
5651
5665
 
5666
+ // ../../node_modules/.pnpm/@earendil-works+pi-tui@0.75.5/node_modules/@earendil-works/pi-tui/dist/fuzzy.js
5667
+ function fuzzyMatch(query, text) {
5668
+ const queryLower = query.toLowerCase();
5669
+ const textLower = text.toLowerCase();
5670
+ const matchQuery = (normalizedQuery) => {
5671
+ if (normalizedQuery.length === 0) {
5672
+ return { matches: true, score: 0 };
5673
+ }
5674
+ if (normalizedQuery.length > textLower.length) {
5675
+ return { matches: false, score: 0 };
5676
+ }
5677
+ let queryIndex = 0;
5678
+ let score = 0;
5679
+ let lastMatchIndex = -1;
5680
+ let consecutiveMatches = 0;
5681
+ for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) {
5682
+ if (textLower[i] === normalizedQuery[queryIndex]) {
5683
+ const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]);
5684
+ if (lastMatchIndex === i - 1) {
5685
+ consecutiveMatches++;
5686
+ score -= consecutiveMatches * 5;
5687
+ } else {
5688
+ consecutiveMatches = 0;
5689
+ if (lastMatchIndex >= 0) {
5690
+ score += (i - lastMatchIndex - 1) * 2;
5691
+ }
5692
+ }
5693
+ if (isWordBoundary) {
5694
+ score -= 10;
5695
+ }
5696
+ score += i * 0.1;
5697
+ lastMatchIndex = i;
5698
+ queryIndex++;
5699
+ }
5700
+ }
5701
+ if (queryIndex < normalizedQuery.length) {
5702
+ return { matches: false, score: 0 };
5703
+ }
5704
+ if (normalizedQuery === textLower) {
5705
+ score -= 100;
5706
+ }
5707
+ return { matches: true, score };
5708
+ };
5709
+ const primaryMatch = matchQuery(queryLower);
5710
+ if (primaryMatch.matches) {
5711
+ return primaryMatch;
5712
+ }
5713
+ const alphaNumericMatch = queryLower.match(/^(?<letters>[a-z]+)(?<digits>[0-9]+)$/);
5714
+ const numericAlphaMatch = queryLower.match(/^(?<digits>[0-9]+)(?<letters>[a-z]+)$/);
5715
+ const swappedQuery = alphaNumericMatch ? `${alphaNumericMatch.groups?.digits ?? ""}${alphaNumericMatch.groups?.letters ?? ""}` : numericAlphaMatch ? `${numericAlphaMatch.groups?.letters ?? ""}${numericAlphaMatch.groups?.digits ?? ""}` : "";
5716
+ if (!swappedQuery) {
5717
+ return primaryMatch;
5718
+ }
5719
+ const swappedMatch = matchQuery(swappedQuery);
5720
+ if (!swappedMatch.matches) {
5721
+ return primaryMatch;
5722
+ }
5723
+ return { matches: true, score: swappedMatch.score + 5 };
5724
+ }
5725
+
5726
+ // src/front/chat/session/piSessionSearch.ts
5727
+ function normalizeWhitespaceLower(text) {
5728
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
5729
+ }
5730
+ function sessionModifiedMs(session) {
5731
+ const value = session.updatedAt;
5732
+ if (value instanceof Date) return value.getTime();
5733
+ if (typeof value === "number") return Number.isFinite(value) ? value : 0;
5734
+ if (typeof value === "string") {
5735
+ const parsed = Date.parse(value);
5736
+ return Number.isFinite(parsed) ? parsed : 0;
5737
+ }
5738
+ return 0;
5739
+ }
5740
+ function getSessionSearchText(session) {
5741
+ const name = session.name ?? session.title ?? "";
5742
+ return `${session.id} ${name} ${session.allMessagesText ?? ""} ${session.cwd ?? ""}`;
5743
+ }
5744
+ function parsePiSessionSearchQuery(query) {
5745
+ const trimmed = query.trim();
5746
+ if (!trimmed) return { mode: "tokens", tokens: [], regex: null };
5747
+ if (trimmed.startsWith("re:")) {
5748
+ const pattern = trimmed.slice(3).trim();
5749
+ if (!pattern) return { mode: "regex", tokens: [], regex: null, error: "Empty regex" };
5750
+ try {
5751
+ return { mode: "regex", tokens: [], regex: new RegExp(pattern, "i") };
5752
+ } catch (error) {
5753
+ return {
5754
+ mode: "regex",
5755
+ tokens: [],
5756
+ regex: null,
5757
+ error: error instanceof Error ? error.message : String(error)
5758
+ };
5759
+ }
5760
+ }
5761
+ const tokens = [];
5762
+ let buffer = "";
5763
+ let inQuote = false;
5764
+ let hadUnclosedQuote = false;
5765
+ const flush = (kind) => {
5766
+ const value = buffer.trim();
5767
+ buffer = "";
5768
+ if (value) tokens.push({ kind, value });
5769
+ };
5770
+ for (let index = 0; index < trimmed.length; index += 1) {
5771
+ const char = trimmed[index];
5772
+ if (char === '"') {
5773
+ if (inQuote) {
5774
+ flush("phrase");
5775
+ inQuote = false;
5776
+ } else {
5777
+ flush("fuzzy");
5778
+ inQuote = true;
5779
+ }
5780
+ continue;
5781
+ }
5782
+ if (!inQuote && /\s/.test(char)) {
5783
+ flush("fuzzy");
5784
+ continue;
5785
+ }
5786
+ buffer += char;
5787
+ }
5788
+ if (inQuote) hadUnclosedQuote = true;
5789
+ if (hadUnclosedQuote) {
5790
+ return {
5791
+ mode: "tokens",
5792
+ tokens: trimmed.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0).map((value) => ({ kind: "fuzzy", value })),
5793
+ regex: null
5794
+ };
5795
+ }
5796
+ flush(inQuote ? "phrase" : "fuzzy");
5797
+ return { mode: "tokens", tokens, regex: null };
5798
+ }
5799
+ function matchPiSessionSearch(session, parsed) {
5800
+ const text = getSessionSearchText(session);
5801
+ if (parsed.mode === "regex") {
5802
+ if (!parsed.regex) return { matches: false, score: 0 };
5803
+ const index = text.search(parsed.regex);
5804
+ if (index < 0) return { matches: false, score: 0 };
5805
+ return { matches: true, score: index * 0.1 };
5806
+ }
5807
+ if (parsed.tokens.length === 0) return { matches: true, score: 0 };
5808
+ let totalScore = 0;
5809
+ let normalizedText = null;
5810
+ for (const token of parsed.tokens) {
5811
+ if (token.kind === "phrase") {
5812
+ normalizedText ??= normalizeWhitespaceLower(text);
5813
+ const phrase = normalizeWhitespaceLower(token.value);
5814
+ if (!phrase) continue;
5815
+ const index = normalizedText.indexOf(phrase);
5816
+ if (index < 0) return { matches: false, score: 0 };
5817
+ totalScore += index * 0.1;
5818
+ continue;
5819
+ }
5820
+ const match = fuzzyMatch(token.value, text);
5821
+ if (!match.matches) return { matches: false, score: 0 };
5822
+ totalScore += match.score;
5823
+ }
5824
+ return { matches: true, score: totalScore };
5825
+ }
5826
+ function searchPiSessions(sessions, query, options = {}) {
5827
+ const { sortMode = "fuzzy", limit } = options;
5828
+ const trimmed = query.trim();
5829
+ const source = [...sessions];
5830
+ if (!trimmed) return limit == null ? source : source.slice(0, limit);
5831
+ const parsed = parsePiSessionSearchQuery(trimmed);
5832
+ if (parsed.error) return [];
5833
+ if (sortMode === "recent") {
5834
+ const filtered = source.filter((session) => matchPiSessionSearch(session, parsed).matches);
5835
+ return limit == null ? filtered : filtered.slice(0, limit);
5836
+ }
5837
+ const scored = [];
5838
+ for (const session of source) {
5839
+ const result = matchPiSessionSearch(session, parsed);
5840
+ if (result.matches) scored.push({ session, score: result.score });
5841
+ }
5842
+ scored.sort((a, b) => {
5843
+ if (a.score !== b.score) return a.score - b.score;
5844
+ return sessionModifiedMs(b.session) - sessionModifiedMs(a.session);
5845
+ });
5846
+ const results = scored.map((result) => result.session);
5847
+ return limit == null ? results : results.slice(0, limit);
5848
+ }
5849
+
5652
5850
  // src/front/chat/components/PiConversationSurface.tsx
5653
5851
  import { Loader2 as Loader22 } from "lucide-react";
5654
5852
  import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect, useRef as useRef10, useState as useState15 } from "react";
@@ -9455,6 +9653,7 @@ function PiChatPanel({
9455
9653
  createRemoteSession,
9456
9654
  remoteSessionOptions,
9457
9655
  hydrateMessages = true,
9656
+ allowPromptDuringInitialHydration = false,
9458
9657
  workspaceWarmupStatus,
9459
9658
  onSessionReset,
9460
9659
  onBeforeSubmit,
@@ -9462,6 +9661,7 @@ function PiChatPanel({
9462
9661
  onCommandResult,
9463
9662
  onComposerWarning,
9464
9663
  onMentionedFilesConsumed,
9664
+ onPromptSubmitStarted,
9465
9665
  onData,
9466
9666
  onOpenArtifact,
9467
9667
  composerBlockers = EMPTY_BLOCKERS,
@@ -9920,6 +10120,7 @@ function PiChatPanel({
9920
10120
  mentionedFiles: effectiveMentionedFiles,
9921
10121
  getDraft: () => draftRef.current,
9922
10122
  onDraftChange: setComposerDraft,
10123
+ allowPromptDuringInitialHydration,
9923
10124
  onPromptSubmitStarted: () => {
9924
10125
  markLocalSubmitted(activeChatSessionId);
9925
10126
  },
@@ -9941,7 +10142,7 @@ function PiChatPanel({
9941
10142
  onMentionedFilesConsumed?.();
9942
10143
  }
9943
10144
  });
9944
- }, [activeChatSessionId, addLocalNotice, clearMentionedFiles, composerBlocked, composerBlockerLabel, effectiveMentionedFiles, markLocalSubmitted, onBeforeSubmit, onCommandResult, onComposerWarning, onMentionedFilesConsumed, openModelPicker, openThinkingPicker, registry, reloadAgentPlugins, resetSession, runPluginUpdate, selectComposerModel, selectComposerThinking, selectedModel, selectedPiSession, selectedThinking, setComposerDraft, submitThinkingControl, suppressPreSubmitCancelledWarning]);
10145
+ }, [activeChatSessionId, addLocalNotice, allowPromptDuringInitialHydration, clearMentionedFiles, composerBlocked, composerBlockerLabel, effectiveMentionedFiles, markLocalSubmitted, onBeforeSubmit, onCommandResult, onComposerWarning, onMentionedFilesConsumed, onPromptSubmitStarted, openModelPicker, openThinkingPicker, registry, reloadAgentPlugins, resetSession, runPluginUpdate, selectComposerModel, selectComposerThinking, selectedModel, selectedPiSession, selectedThinking, setComposerDraft, submitThinkingControl, suppressPreSubmitCancelledWarning]);
9945
10146
  const surfaceRunRejected = useCallback18((error) => {
9946
10147
  const errorCode = piChatErrorCode(error);
9947
10148
  dropLocalNotice(RUN_REJECTED_NOTICE_ID);
@@ -9974,6 +10175,7 @@ function PiChatPanel({
9974
10175
  dropLocalNotice(RUN_REJECTED_NOTICE_ID);
9975
10176
  }
9976
10177
  if (result.type === "prompt" && activeChatSessionId) {
10178
+ onPromptSubmitStarted?.({ sessionId: activeChatSessionId, clientNonce: result.clientNonce });
9977
10179
  if (shouldHoldLocalSubmitted(selectedPiSession, result.cursor)) markLocalSubmitted(activeChatSessionId);
9978
10180
  else clearLocalSubmitted(activeChatSessionId);
9979
10181
  }
@@ -9984,7 +10186,7 @@ function PiChatPanel({
9984
10186
  surfaceRunRejected(error);
9985
10187
  return false;
9986
10188
  }
9987
- }, [activeChatSessionId, clearLocalSubmitted, dropLocalNotice, markLocalSubmitted, policy, selectedPiSession, setComposerDraft, surfaceRunRejected]);
10189
+ }, [activeChatSessionId, clearLocalSubmitted, dropLocalNotice, markLocalSubmitted, onPromptSubmitStarted, policy, selectedPiSession, setComposerDraft, surfaceRunRejected]);
9988
10190
  const editQueued = useCallback18(() => {
9989
10191
  if (!policy) return;
9990
10192
  void policy.editQueued().then((result) => {
@@ -10061,6 +10263,7 @@ function PiChatPanel({
10061
10263
  dropLocalNotice(RUN_REJECTED_NOTICE_ID);
10062
10264
  }
10063
10265
  if (result.type === "prompt") {
10266
+ onPromptSubmitStarted?.({ sessionId: activeSessionId, clientNonce: result.clientNonce });
10064
10267
  if (shouldHoldLocalSubmitted(selectedPiSession, result.cursor)) markLocalSubmitted(activeSessionId);
10065
10268
  else clearLocalSubmitted(activeSessionId);
10066
10269
  }
@@ -10075,15 +10278,18 @@ function PiChatPanel({
10075
10278
  settlePendingAutoSubmit(activeSessionId);
10076
10279
  surfaceRunRejected(error);
10077
10280
  });
10078
- }, [activeSessionId, autoSubmitInitialDraft, clearLocalSubmitted, composerBlocked, dropLocalNotice, initialDraft, markLocalSubmitted, onAutoSubmitInitialDraftAccepted, policy, selectedPiSession, setComposerDraft, settlePendingAutoSubmit, surfaceRunRejected]);
10281
+ }, [activeSessionId, autoSubmitInitialDraft, clearLocalSubmitted, composerBlocked, dropLocalNotice, initialDraft, markLocalSubmitted, onAutoSubmitInitialDraftAccepted, onPromptSubmitStarted, policy, selectedPiSession, setComposerDraft, settlePendingAutoSubmit, surfaceRunRejected]);
10079
10282
  useEffect21(() => {
10080
10283
  if (workspaceWarmupStatus?.status === "ready") {
10081
10284
  clearLocalNotice("workspace-warmup");
10082
10285
  }
10083
10286
  }, [clearLocalNotice, workspaceWarmupStatus?.status]);
10287
+ const initialHydrationPromptAllowed = Boolean(
10288
+ allowPromptDuringInitialHydration && selectedChatState && selectedChatState.status === "hydrating" && !selectedChatState.hydrated && selectedChatState.history.messageCount === 0 && selectedChatState.committedMessages.length === 0 && selectedChatState.queue.followUps.length === 0 && Object.keys(selectedChatState.optimisticOutbox).length === 0 && !selectedChatState.streamingMessage
10289
+ );
10084
10290
  const disabled = !policy || sessionsLoading || composerBlocked;
10085
10291
  const isStreaming = isPiBusyStatus(status);
10086
- const submitStatus = toPromptSubmitStatus(status);
10292
+ const submitStatus = initialHydrationPromptAllowed ? "ready" : toPromptSubmitStatus(status);
10087
10293
  const submitDisabled = !policy || sessionsLoading || composerBlocked && !isStreaming;
10088
10294
  const mergedToolRenderers = useMemo13(() => mergeShadcnToolRenderers(toolRenderers), [toolRenderers]);
10089
10295
  const debugMessages = useMemo13(() => messages.map(toDebugUiMessage), [messages]);
@@ -10336,11 +10542,14 @@ export {
10336
10542
  defaultChatSuggestions,
10337
10543
  defaultToolRenderers,
10338
10544
  getAgentCommands,
10545
+ matchPiSessionSearch,
10339
10546
  mergeShadcnToolRenderers,
10340
10547
  mergeToolRenderers,
10548
+ parsePiSessionSearchQuery,
10341
10549
  parseSlashCommand,
10342
10550
  readActiveSessionId,
10343
10551
  resolveToolRenderer,
10552
+ searchPiSessions,
10344
10553
  uploadFile,
10345
10554
  useOpenArtifact,
10346
10555
  usePiSessions,
@@ -2619,6 +2619,37 @@ function buildFileRecordsResult(args) {
2619
2619
  return result;
2620
2620
  }
2621
2621
 
2622
+ // src/server/http/readonlySkillFiles.ts
2623
+ import { readFile as readFile4, stat as stat4 } from "fs/promises";
2624
+ function isReadonlySkillFilePath(path4) {
2625
+ if (!path4.startsWith("/")) return false;
2626
+ if (path4.includes("\0")) return false;
2627
+ if (!path4.endsWith("/SKILL.md")) return false;
2628
+ return path4.includes("/.pi/agent/") && path4.includes("/skills/");
2629
+ }
2630
+ async function readReadonlySkillFile(path4) {
2631
+ const [content, fsStat2] = await Promise.all([
2632
+ readFile4(path4, "utf-8"),
2633
+ stat4(path4)
2634
+ ]);
2635
+ return {
2636
+ content,
2637
+ stat: {
2638
+ kind: fsStat2.isDirectory() ? "directory" : "file",
2639
+ size: fsStat2.size,
2640
+ mtimeMs: fsStat2.mtimeMs
2641
+ }
2642
+ };
2643
+ }
2644
+ async function statReadonlySkillFile(path4) {
2645
+ const fsStat2 = await stat4(path4);
2646
+ return {
2647
+ kind: fsStat2.isDirectory() ? "directory" : "file",
2648
+ size: fsStat2.size,
2649
+ mtimeMs: fsStat2.mtimeMs
2650
+ };
2651
+ }
2652
+
2622
2653
  // src/server/http/routes/file.ts
2623
2654
  var log2 = createLogger("boring/workspace-settings");
2624
2655
  var BORING_SETTINGS_PATH = ".boring/settings";
@@ -2828,8 +2859,8 @@ function fileRoutes(app, opts, done) {
2828
2859
  error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
2829
2860
  });
2830
2861
  }
2831
- const stat11 = await workspace.stat(path4);
2832
- if (stat11.kind !== "file") {
2862
+ const stat12 = await workspace.stat(path4);
2863
+ if (stat12.kind !== "file") {
2833
2864
  return reply.code(400).send({
2834
2865
  error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
2835
2866
  });
@@ -2844,11 +2875,11 @@ function fileRoutes(app, opts, done) {
2844
2875
  try {
2845
2876
  const parsed = parseFileRecordsRequest(request.query);
2846
2877
  const workspace = await resolveWorkspace(request);
2847
- const stat11 = await workspace.stat(parsed.path);
2848
- if (stat11.kind !== "file") {
2878
+ const stat12 = await workspace.stat(parsed.path);
2879
+ if (stat12.kind !== "file") {
2849
2880
  return sendValidationError(reply, "path is not a file", "path");
2850
2881
  }
2851
- if (stat11.size > MAX_RECORD_FILE_BYTES) {
2882
+ if (stat12.size > MAX_RECORD_FILE_BYTES) {
2852
2883
  return sendValidationError(reply, "records file is too large", "path");
2853
2884
  }
2854
2885
  const content = await workspace.readFile(parsed.path);
@@ -2858,7 +2889,7 @@ function fileRoutes(app, opts, done) {
2858
2889
  return buildFileRecordsResult({
2859
2890
  path: parsed.path,
2860
2891
  content,
2861
- mtimeMs: stat11.mtimeMs,
2892
+ mtimeMs: stat12.mtimeMs,
2862
2893
  offset: parsed.offset,
2863
2894
  limit: parsed.limit,
2864
2895
  q: parsed.q
@@ -2875,14 +2906,23 @@ function fileRoutes(app, opts, done) {
2875
2906
  const path4 = requireStringParam(query.path, "path", reply);
2876
2907
  if (path4 === null) return;
2877
2908
  try {
2909
+ if (isReadonlySkillFilePath(path4)) {
2910
+ const { content: content2, stat: stat13 } = await readReadonlySkillFile(path4);
2911
+ if (stat13.kind !== "file") {
2912
+ return reply.code(400).send({
2913
+ error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
2914
+ });
2915
+ }
2916
+ return { content: content2, mtimeMs: stat13.mtimeMs };
2917
+ }
2878
2918
  const workspace = await resolveWorkspace(request);
2879
2919
  if (workspace.readFileWithStat) {
2880
- const { content: content2, stat: stat12 } = await workspace.readFileWithStat(path4);
2881
- return { content: content2, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
2920
+ const { content: content2, stat: stat13 } = await workspace.readFileWithStat(path4);
2921
+ return { content: content2, mtimeMs: stat13.kind === "file" ? stat13.mtimeMs : void 0 };
2882
2922
  }
2883
2923
  const content = await workspace.readFile(path4);
2884
- const stat11 = await workspace.stat(path4);
2885
- return { content, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
2924
+ const stat12 = await workspace.stat(path4);
2925
+ return { content, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
2886
2926
  } catch (err) {
2887
2927
  return classifyError(err, reply, "file");
2888
2928
  }
@@ -2936,11 +2976,11 @@ function fileRoutes(app, opts, done) {
2936
2976
  await workspace.writeFile(path4, content);
2937
2977
  return { ok: true };
2938
2978
  }
2939
- const stat11 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
2979
+ const stat12 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
2940
2980
  await workspace.writeFile(path4, content);
2941
2981
  return await workspace.stat(path4);
2942
2982
  })();
2943
- return { ok: true, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
2983
+ return { ok: true, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
2944
2984
  } catch (err) {
2945
2985
  return classifyError(err, reply, "file");
2946
2986
  }
@@ -2969,7 +3009,7 @@ function fileRoutes(app, opts, done) {
2969
3009
  }
2970
3010
  const bytes = Buffer.from(contentBase64, "base64");
2971
3011
  await workspace.mkdir(dir, { recursive: true });
2972
- const stat11 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
3012
+ const stat12 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
2973
3013
  if (!workspace.writeBinaryFile) {
2974
3014
  throw new Error("workspace does not support binary uploads");
2975
3015
  }
@@ -2983,7 +3023,7 @@ function fileRoutes(app, opts, done) {
2983
3023
  ok: true,
2984
3024
  path: path4,
2985
3025
  markdownUrl: markdownUrlFor(sourcePath, path4),
2986
- mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0
3026
+ mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0
2987
3027
  };
2988
3028
  } catch (err) {
2989
3029
  return classifyError(err, reply, "upload");
@@ -3069,9 +3109,12 @@ function fileRoutes(app, opts, done) {
3069
3109
  const path4 = requireStringParam(query.path, "path", reply);
3070
3110
  if (path4 === null) return;
3071
3111
  try {
3112
+ if (isReadonlySkillFilePath(path4)) {
3113
+ return await statReadonlySkillFile(path4);
3114
+ }
3072
3115
  const workspace = await resolveWorkspace(request);
3073
- const stat11 = await workspace.stat(path4);
3074
- return stat11;
3116
+ const stat12 = await workspace.stat(path4);
3117
+ return stat12;
3075
3118
  } catch (err) {
3076
3119
  return classifyError(err, reply, "path");
3077
3120
  }
@@ -3082,7 +3125,7 @@ function fileRoutes(app, opts, done) {
3082
3125
  // src/server/workspace/provisionRuntime.ts
3083
3126
  import { createHash as createHash3 } from "crypto";
3084
3127
  import { constants as constants2 } from "fs";
3085
- import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, realpath as realpath2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
3128
+ import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, realpath as realpath2, stat as stat5, writeFile as writeFile4 } from "fs/promises";
3086
3129
  import { dirname as dirname6, isAbsolute as isAbsolute4, join as join4, relative as relative5, resolve as resolve5 } from "path";
3087
3130
  import { fileURLToPath } from "url";
3088
3131
  import { execFile } from "child_process";
@@ -3117,7 +3160,7 @@ async function commandExists(cmd) {
3117
3160
  }
3118
3161
  }
3119
3162
  async function hashPath(path4, hash) {
3120
- const info = await stat4(path4);
3163
+ const info = await stat5(path4);
3121
3164
  if (info.isDirectory()) {
3122
3165
  const entries = (await readdir3(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
3123
3166
  for (const entry of entries) {
@@ -3130,7 +3173,7 @@ async function hashPath(path4, hash) {
3130
3173
  if (!info.isFile()) return;
3131
3174
  hash.update(`file:${path4}
3132
3175
  `);
3133
- hash.update(await readFile4(path4));
3176
+ hash.update(await readFile5(path4));
3134
3177
  }
3135
3178
  async function fingerprint(contributions) {
3136
3179
  const hash = createHash3("sha256");
@@ -3323,7 +3366,7 @@ VENV_BIN="$WORKSPACE_ROOT/.boring-agent/venv/bin"
3323
3366
  for (const entry of await readdir3(venvBin)) {
3324
3367
  if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
3325
3368
  const full = join4(venvBin, entry);
3326
- const info = await stat4(full).catch(() => null);
3369
+ const info = await stat5(full).catch(() => null);
3327
3370
  if (!info?.isFile()) continue;
3328
3371
  await writeExecutable(join4(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
3329
3372
  SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
@@ -3352,7 +3395,7 @@ async function provisionRuntimeWorkspace({
3352
3395
  const binDir = join4(workspaceRoot, ".boring-agent", "bin");
3353
3396
  if (!force && await exists(markerPath)) {
3354
3397
  try {
3355
- const marker = JSON.parse(await readFile4(markerPath, "utf8"));
3398
+ const marker = JSON.parse(await readFile5(markerPath, "utf8"));
3356
3399
  if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
3357
3400
  await writeShims(workspaceRoot, env);
3358
3401
  return { fingerprint: hash, changed: false, env, binDir };
@@ -3410,11 +3453,11 @@ function createTimedLruCache(ttlMs, maxEntries) {
3410
3453
  }
3411
3454
  };
3412
3455
  }
3413
- function cloneStat(stat11) {
3456
+ function cloneStat(stat12) {
3414
3457
  return {
3415
- size: stat11.size,
3416
- mtimeMs: stat11.mtimeMs,
3417
- kind: stat11.kind
3458
+ size: stat12.size,
3459
+ mtimeMs: stat12.mtimeMs,
3460
+ kind: stat12.kind
3418
3461
  };
3419
3462
  }
3420
3463
  function cloneEntries(entries) {
@@ -3622,15 +3665,15 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3622
3665
  remote.fs.readFile(sandboxPath, "utf8"),
3623
3666
  remote.fs.stat(sandboxPath)
3624
3667
  ]);
3625
- const stat11 = {
3668
+ const stat12 = {
3626
3669
  size: fileStat.size,
3627
3670
  mtimeMs: fileStat.mtimeMs,
3628
3671
  kind: fileStat.isDirectory() ? "dir" : "file"
3629
3672
  };
3630
- statCache.set(sandboxPath, stat11);
3673
+ statCache.set(sandboxPath, stat12);
3631
3674
  return {
3632
3675
  content: typeof content === "string" ? content : Buffer.from(content).toString("utf-8"),
3633
- stat: cloneStat(stat11)
3676
+ stat: cloneStat(stat12)
3634
3677
  };
3635
3678
  }
3636
3679
  const version = metadataVersion;
@@ -3896,7 +3939,7 @@ function createVercelProvisioningAdapter(options) {
3896
3939
  // src/server/workspace/provisioning/fingerprint.ts
3897
3940
  import { createHash as createHash4 } from "crypto";
3898
3941
  import { constants as constants3 } from "fs";
3899
- import { access as access3, mkdir as mkdir6, open, readFile as readFile5, rename as rename5, rm as rm2 } from "fs/promises";
3942
+ import { access as access3, mkdir as mkdir6, open, readFile as readFile6, rename as rename5, rm as rm2 } from "fs/promises";
3900
3943
  import { dirname as dirname8 } from "path";
3901
3944
  var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
3902
3945
  function isValidFingerprint(value) {
@@ -4278,7 +4321,7 @@ async function ensurePythonRuntime(options) {
4278
4321
  }
4279
4322
 
4280
4323
  // src/server/workspace/provisioning/skills.ts
4281
- import { stat as stat5 } from "fs/promises";
4324
+ import { stat as stat6 } from "fs/promises";
4282
4325
  import { join as join8 } from "path";
4283
4326
  import { fileURLToPath as fileURLToPath4 } from "url";
4284
4327
  var GENERATED_SKILLS_REL = ".boring-agent/skills";
@@ -4309,7 +4352,7 @@ async function mirrorPluginSkills(options) {
4309
4352
  }
4310
4353
  seen.add(key);
4311
4354
  const sourcePath = sourceToPath2(skill.source);
4312
- const sourceStat = await stat5(sourcePath);
4355
+ const sourceStat = await stat6(sourcePath);
4313
4356
  const skillTarget = `${GENERATED_SKILLS_REL}/${plugin.id}/${skill.name}`;
4314
4357
  const target = sourceStat.isDirectory() ? skillTarget : `${skillTarget}/SKILL.md`;
4315
4358
  await options.adapter.workspaceFs.copyFromHost(skill.source, target);
@@ -4325,7 +4368,7 @@ async function mirrorPluginSkills(options) {
4325
4368
  // src/server/workspace/provisioning/workspaceFiles.ts
4326
4369
  import { basename, join as joinPath } from "path";
4327
4370
  import { posix as posix2 } from "path";
4328
- import { readdir as readdir4, stat as stat6 } from "fs/promises";
4371
+ import { readdir as readdir4, stat as stat7 } from "fs/promises";
4329
4372
  import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
4330
4373
  function sourceToPath3(source) {
4331
4374
  return source instanceof URL ? fileURLToPath5(source) : source;
@@ -4338,7 +4381,7 @@ function sourceForChild(rootSource, childPath) {
4338
4381
  }
4339
4382
  async function collectTemplateWorkItems(template) {
4340
4383
  const sourcePath = sourceToPath3(template.path);
4341
- const sourceStat = await stat6(sourcePath);
4384
+ const sourceStat = await stat7(sourcePath);
4342
4385
  const targetPrefix = template.target ?? "";
4343
4386
  if (!sourceStat.isDirectory()) {
4344
4387
  const targetRel = toWorkspaceRel3(targetPrefix || basename(sourcePath));
@@ -4645,7 +4688,7 @@ async function copyTemplate(templatePath, workspaceRoot) {
4645
4688
 
4646
4689
  // src/server/runtime/modes/provisioningAdapter.ts
4647
4690
  import { spawn as spawn3 } from "child_process";
4648
- import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as readFile6, realpath as realpath3, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
4691
+ import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as readFile7, realpath as realpath3, rm as rm3, stat as stat8, writeFile as writeFile5 } from "fs/promises";
4649
4692
  import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
4650
4693
  import { fileURLToPath as fileURLToPath6 } from "url";
4651
4694
  var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
@@ -4754,7 +4797,7 @@ function createWorkspaceFs(workspaceRoot, opts) {
4754
4797
  async exists(workspaceRelativePath) {
4755
4798
  const absPath = validatePath(workspaceRoot, workspaceRelativePath);
4756
4799
  try {
4757
- await stat7(absPath);
4800
+ await stat8(absPath);
4758
4801
  return true;
4759
4802
  } catch (error) {
4760
4803
  if (error.code === "ENOENT") return false;
@@ -4780,12 +4823,12 @@ function createWorkspaceFs(workspaceRoot, opts) {
4780
4823
  async readText(workspaceRelativePath) {
4781
4824
  const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
4782
4825
  if (!absPath) return null;
4783
- return await readFile6(absPath, "utf8");
4826
+ return await readFile7(absPath, "utf8");
4784
4827
  },
4785
4828
  async copyFromHost(hostSourcePath, workspaceRelativeTarget) {
4786
4829
  const sourcePath = sourceToPath4(hostSourcePath);
4787
4830
  const absTarget = await prepareWritablePath(workspaceRoot, workspaceRelativeTarget, enforceSymlinkBoundary);
4788
- const sourceStat = await stat7(sourcePath);
4831
+ const sourceStat = await stat8(sourcePath);
4789
4832
  await cp3(sourcePath, absTarget, {
4790
4833
  recursive: sourceStat.isDirectory(),
4791
4834
  force: false,
@@ -4915,7 +4958,7 @@ var localModeAdapter = {
4915
4958
  };
4916
4959
 
4917
4960
  // src/server/runtime/modes/vercel-sandbox.ts
4918
- import { readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
4961
+ import { readFile as readFile9, readdir as readdir6, stat as stat9 } from "fs/promises";
4919
4962
  import { join as join9 } from "path";
4920
4963
  import { fileURLToPath as fileURLToPath7 } from "url";
4921
4964
  import { Sandbox as VercelSandbox } from "@vercel/sandbox";
@@ -5062,7 +5105,7 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
5062
5105
 
5063
5106
  // src/server/sandbox/vercel-sandbox/packageTemplate.ts
5064
5107
  import { createHash as createHash5 } from "crypto";
5065
- import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
5108
+ import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
5066
5109
  import path3 from "path";
5067
5110
  import { Readable } from "stream";
5068
5111
  import { createGzip } from "zlib";
@@ -5083,7 +5126,7 @@ async function collectFiles(dir, base = "") {
5083
5126
  if (entry.isDirectory()) {
5084
5127
  files.push(...await collectFiles(fullPath, relPath));
5085
5128
  } else if (entry.isFile()) {
5086
- files.push({ rel: relPath, content: await readFile7(fullPath) });
5129
+ files.push({ rel: relPath, content: await readFile8(fullPath) });
5087
5130
  }
5088
5131
  }
5089
5132
  return files.sort((a, b) => a.rel.localeCompare(b.rel));
@@ -5360,7 +5403,7 @@ function shellSingleQuote(value) {
5360
5403
  }
5361
5404
  async function copyHostPathToVercelWorkspace(options) {
5362
5405
  const sourcePath = provisioningSourceToPath(options.source);
5363
- const sourceStat = await stat8(sourcePath);
5406
+ const sourceStat = await stat9(sourcePath);
5364
5407
  if (sourceStat.isDirectory()) {
5365
5408
  await options.workspace.mkdir(options.targetRel, { recursive: true });
5366
5409
  for (const entry of await readdir6(sourcePath, { withFileTypes: true })) {
@@ -5373,7 +5416,7 @@ async function copyHostPathToVercelWorkspace(options) {
5373
5416
  return;
5374
5417
  }
5375
5418
  if (!sourceStat.isFile()) return;
5376
- const bytes = new Uint8Array(await readFile8(sourcePath));
5419
+ const bytes = new Uint8Array(await readFile9(sourcePath));
5377
5420
  if (options.workspace.writeBinaryFile) await options.workspace.writeBinaryFile(options.targetRel, bytes);
5378
5421
  else await options.workspace.writeFile(options.targetRel, Buffer.from(bytes).toString("utf8"));
5379
5422
  }
@@ -6159,7 +6202,7 @@ function createPiAgentSessionAdapter(session, options = {}) {
6159
6202
  import { randomUUID } from "crypto";
6160
6203
  import {
6161
6204
  readdir as readdir7,
6162
- readFile as readFile9,
6205
+ readFile as readFile10,
6163
6206
  stat as fsStat,
6164
6207
  rm as rm4,
6165
6208
  mkdir as mkdir11,
@@ -6322,7 +6365,7 @@ var PiSessionStore = class {
6322
6365
  const filepath = await this.resolveSessionFile(sessionId);
6323
6366
  let content;
6324
6367
  try {
6325
- content = await readFile9(filepath, "utf-8");
6368
+ content = await readFile10(filepath, "utf-8");
6326
6369
  } catch {
6327
6370
  throw new Error(`Session not found: ${sessionId}`);
6328
6371
  }
@@ -6403,7 +6446,7 @@ var PiSessionStore = class {
6403
6446
  let filepath = direct;
6404
6447
  let content;
6405
6448
  try {
6406
- content = await readFile9(direct, "utf-8");
6449
+ content = await readFile10(direct, "utf-8");
6407
6450
  } catch {
6408
6451
  const files = await readdir7(this.sessionDir).catch(() => []);
6409
6452
  const match = files.find(
@@ -6411,7 +6454,7 @@ var PiSessionStore = class {
6411
6454
  );
6412
6455
  if (!match) return null;
6413
6456
  filepath = join10(this.sessionDir, match);
6414
- content = await readFile9(filepath, "utf-8");
6457
+ content = await readFile10(filepath, "utf-8");
6415
6458
  }
6416
6459
  const entries = safeParseEntries(content);
6417
6460
  const linkedPiFile = extractPiSessionFilePath(entries);
@@ -6488,7 +6531,7 @@ var PiSessionStore = class {
6488
6531
  }
6489
6532
  async linkedPiFileFor(filepath) {
6490
6533
  try {
6491
- const content = await readFile9(filepath, "utf-8");
6534
+ const content = await readFile10(filepath, "utf-8");
6492
6535
  return extractPiSessionFilePath(safeParseEntries(content));
6493
6536
  } catch {
6494
6537
  return null;
@@ -6496,9 +6539,9 @@ var PiSessionStore = class {
6496
6539
  }
6497
6540
  async referencedPiFiles(files) {
6498
6541
  const referenced = /* @__PURE__ */ new Set();
6499
- await Promise.all(files.map(async ({ filepath, stat: stat11 }) => {
6542
+ await Promise.all(files.map(async ({ filepath, stat: stat12 }) => {
6500
6543
  try {
6501
- const piFilePath = (await this.readPrefixCache(filepath, stat11)).referencedPiFile;
6544
+ const piFilePath = (await this.readPrefixCache(filepath, stat12)).referencedPiFile;
6502
6545
  if (piFilePath && resolve7(piFilePath) !== resolve7(filepath)) {
6503
6546
  referenced.add(resolve7(piFilePath));
6504
6547
  }
@@ -6507,10 +6550,10 @@ var PiSessionStore = class {
6507
6550
  }));
6508
6551
  return referenced;
6509
6552
  }
6510
- async sessionSortMtimeMs({ filepath, stat: stat11 }) {
6511
- let sortMtimeMs = stat11.mtime.getTime();
6553
+ async sessionSortMtimeMs({ filepath, stat: stat12 }) {
6554
+ let sortMtimeMs = stat12.mtime.getTime();
6512
6555
  try {
6513
- const linkedPiFile = (await this.readPrefixCache(filepath, stat11)).referencedPiFile;
6556
+ const linkedPiFile = (await this.readPrefixCache(filepath, stat12)).referencedPiFile;
6514
6557
  if (linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath)) {
6515
6558
  const linkedStat = await fsStat(linkedPiFile);
6516
6559
  sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
@@ -6604,7 +6647,7 @@ var PiSessionStore = class {
6604
6647
  const batch = visibleFiles.slice(index, index + batchSize);
6605
6648
  index += batch.length;
6606
6649
  const summaries = await Promise.all(
6607
- batch.map(({ filepath, stat: stat11 }) => this.summarizeFile(filepath, stat11))
6650
+ batch.map(({ filepath, stat: stat12 }) => this.summarizeFile(filepath, stat12))
6608
6651
  );
6609
6652
  for (const summary of summaries) {
6610
6653
  if (!summary) continue;
@@ -6706,7 +6749,7 @@ var PiSessionStore = class {
6706
6749
  try {
6707
6750
  const [fileStat, content] = await Promise.all([
6708
6751
  fsStat(filepath),
6709
- readFile9(filepath, "utf-8")
6752
+ readFile10(filepath, "utf-8")
6710
6753
  ]);
6711
6754
  return { entries: safeParseEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
6712
6755
  } catch {
@@ -7449,7 +7492,7 @@ ${entry.message}`;
7449
7492
  }
7450
7493
 
7451
7494
  // src/server/harness/pi-coding-agent/pluginLoader.ts
7452
- import { readdir as readdir8, stat as stat9, readFile as readFile10 } from "fs/promises";
7495
+ import { readdir as readdir8, stat as stat10, readFile as readFile11 } from "fs/promises";
7453
7496
  import { join as join12, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
7454
7497
  import { homedir as homedir4 } from "os";
7455
7498
  import { pathToFileURL as pathToFileURL2 } from "url";
@@ -7459,7 +7502,7 @@ var LOCAL_DIR = ".pi/extensions";
7459
7502
  var EXTENSIONS_JSON = ".pi/extensions.json";
7460
7503
  async function dirExists2(path4) {
7461
7504
  try {
7462
- const s = await stat9(path4);
7505
+ const s = await stat10(path4);
7463
7506
  return s.isDirectory();
7464
7507
  } catch {
7465
7508
  return false;
@@ -7467,7 +7510,7 @@ async function dirExists2(path4) {
7467
7510
  }
7468
7511
  async function fileExists(path4) {
7469
7512
  try {
7470
- const s = await stat9(path4);
7513
+ const s = await stat10(path4);
7471
7514
  return s.isFile();
7472
7515
  } catch {
7473
7516
  return false;
@@ -7518,7 +7561,7 @@ async function discoverNpmPlugins(cwd) {
7518
7561
  async function loadNpmPlugin(pkgDir, importFn) {
7519
7562
  const pkgJsonPath = join12(pkgDir, "package.json");
7520
7563
  if (!await fileExists(pkgJsonPath)) return [];
7521
- const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
7564
+ const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
7522
7565
  const main = pkgJson.main ?? "index.js";
7523
7566
  const resolvedMain = resolve8(pkgDir, main);
7524
7567
  const resolvedPkgDir = resolve8(pkgDir);
@@ -7535,7 +7578,7 @@ async function loadExtensionsJson(cwd) {
7535
7578
  const configPath = join12(cwd, EXTENSIONS_JSON);
7536
7579
  if (!await fileExists(configPath)) return null;
7537
7580
  try {
7538
- const raw = await readFile10(configPath, "utf-8");
7581
+ const raw = await readFile11(configPath, "utf-8");
7539
7582
  return JSON.parse(raw);
7540
7583
  } catch {
7541
7584
  return null;
@@ -7609,7 +7652,7 @@ import {
7609
7652
 
7610
7653
  // src/server/tools/operations/bound.ts
7611
7654
  import { constants as constants4 } from "fs";
7612
- import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir9, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile7 } from "fs/promises";
7655
+ import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile12, readdir as readdir9, readlink, realpath as realpath4, stat as stat11, writeFile as writeFile7 } from "fs/promises";
7613
7656
  import { dirname as dirname11, isAbsolute as isAbsolute7, join as join13, relative as relative10, resolve as resolve9 } from "path";
7614
7657
  function toPosixPath(value) {
7615
7658
  return value.split("\\").join("/");
@@ -7681,7 +7724,7 @@ async function findNearestExistingAncestor(absPath) {
7681
7724
  let current = absPath;
7682
7725
  for (; ; ) {
7683
7726
  try {
7684
- await stat10(current);
7727
+ await stat11(current);
7685
7728
  return current;
7686
7729
  } catch {
7687
7730
  const parent = dirname11(current);
@@ -7756,7 +7799,7 @@ function boundFs(workspaceRoot, opts = {}) {
7756
7799
  async readFile(absolutePath) {
7757
7800
  const storagePath = toStoragePath(absolutePath);
7758
7801
  await assertWithinWorkspace(workspaceRoot, storagePath);
7759
- return await readFile11(storagePath);
7802
+ return await readFile12(storagePath);
7760
7803
  },
7761
7804
  async access(absolutePath) {
7762
7805
  const storagePath = toStoragePath(absolutePath);
@@ -7781,7 +7824,7 @@ function boundFs(workspaceRoot, opts = {}) {
7781
7824
  async readFile(absolutePath) {
7782
7825
  const storagePath = toStoragePath(absolutePath);
7783
7826
  await assertWithinWorkspace(workspaceRoot, storagePath);
7784
- return await readFile11(storagePath);
7827
+ return await readFile12(storagePath);
7785
7828
  },
7786
7829
  async writeFile(absolutePath, content) {
7787
7830
  const storagePath = toStoragePath(absolutePath);
@@ -7799,7 +7842,7 @@ function boundFs(workspaceRoot, opts = {}) {
7799
7842
  const storagePath = toStoragePath(absolutePath);
7800
7843
  await assertWithinWorkspace(workspaceRoot, storagePath);
7801
7844
  try {
7802
- await stat10(storagePath);
7845
+ await stat11(storagePath);
7803
7846
  return true;
7804
7847
  } catch (err) {
7805
7848
  if (err.code === "ENOENT") return false;
@@ -7818,12 +7861,12 @@ function boundFs(workspaceRoot, opts = {}) {
7818
7861
  async isDirectory(absolutePath) {
7819
7862
  const storagePath = toStoragePath(absolutePath);
7820
7863
  await assertWithinWorkspace(workspaceRoot, storagePath);
7821
- return (await stat10(storagePath)).isDirectory();
7864
+ return (await stat11(storagePath)).isDirectory();
7822
7865
  },
7823
7866
  async readFile(absolutePath) {
7824
7867
  const storagePath = toStoragePath(absolutePath);
7825
7868
  await assertWithinWorkspace(workspaceRoot, storagePath);
7826
- return await readFile11(storagePath, "utf8");
7869
+ return await readFile12(storagePath, "utf8");
7827
7870
  }
7828
7871
  };
7829
7872
  const ls = {
@@ -7831,7 +7874,7 @@ function boundFs(workspaceRoot, opts = {}) {
7831
7874
  const storagePath = toStoragePath(absolutePath);
7832
7875
  try {
7833
7876
  await assertWithinWorkspace(workspaceRoot, storagePath);
7834
- await stat10(storagePath);
7877
+ await stat11(storagePath);
7835
7878
  return true;
7836
7879
  } catch {
7837
7880
  return false;
@@ -7840,7 +7883,7 @@ function boundFs(workspaceRoot, opts = {}) {
7840
7883
  async stat(absolutePath) {
7841
7884
  const storagePath = toStoragePath(absolutePath);
7842
7885
  await assertWithinWorkspace(workspaceRoot, storagePath);
7843
- return await stat10(storagePath);
7886
+ return await stat11(storagePath);
7844
7887
  },
7845
7888
  async readdir(absolutePath) {
7846
7889
  const storagePath = toStoragePath(absolutePath);
@@ -8023,8 +8066,8 @@ function remoteWorkspaceLsOps(workspace, opts = {}) {
8023
8066
  },
8024
8067
  async stat(absolutePath) {
8025
8068
  const rel = toRelPath(workspace, absolutePath, opts);
8026
- const stat11 = await workspace.stat(rel);
8027
- return { isDirectory: () => stat11.kind === "dir" };
8069
+ const stat12 = await workspace.stat(rel);
8070
+ return { isDirectory: () => stat12.kind === "dir" };
8028
8071
  },
8029
8072
  async readdir(absolutePath) {
8030
8073
  const rel = toRelPath(workspace, absolutePath, opts);
@@ -9092,49 +9135,53 @@ import {
9092
9135
  var CACHE_TTL_MS2 = 3e4;
9093
9136
  function skillsRoutes(app, opts, done) {
9094
9137
  const cached = /* @__PURE__ */ new Map();
9095
- app.get("/api/v1/agent/skills", async (request, reply) => {
9096
- try {
9097
- const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
9098
- const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
9099
- const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
9100
- const noSkills = (opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills) ?? withPiHarnessDefaults().noSkills;
9101
- const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], noSkills]);
9102
- const now = Date.now();
9103
- for (const [key, entry] of cached) {
9104
- if (entry.expiresAt <= now) cached.delete(key);
9105
- }
9106
- const cachedEntry = cached.get(cacheKey);
9107
- const refresh = request.query.refresh === "1";
9108
- if (!refresh && cachedEntry && cachedEntry.expiresAt > now) {
9109
- return reply.code(200).send({ skills: cachedEntry.skills });
9110
- }
9111
- const agentDir = getAgentDir3();
9112
- const packageSkillPaths = noSkills ? [] : await (async () => {
9113
- const settingsManager = createResourceSettingsManager(
9114
- workspaceRoot,
9115
- agentDir,
9116
- piPackages ?? []
9117
- );
9118
- const packageManager = new DefaultPackageManager({
9119
- cwd: workspaceRoot,
9120
- agentDir,
9121
- settingsManager
9122
- });
9123
- const resolved = await packageManager.resolve();
9124
- return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
9125
- })();
9126
- const result = loadSkills2({
9138
+ async function resolveSkillsForRequest(request, refresh = false) {
9139
+ const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
9140
+ const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
9141
+ const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
9142
+ const noSkills = (opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills) ?? withPiHarnessDefaults().noSkills;
9143
+ const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], noSkills]);
9144
+ const now = Date.now();
9145
+ for (const [key, entry2] of cached) {
9146
+ if (entry2.expiresAt <= now) cached.delete(key);
9147
+ }
9148
+ const cachedEntry = cached.get(cacheKey);
9149
+ if (!refresh && cachedEntry && cachedEntry.expiresAt > now) return cachedEntry;
9150
+ const agentDir = getAgentDir3();
9151
+ const packageSkillPaths = noSkills ? [] : await (async () => {
9152
+ const settingsManager = createResourceSettingsManager(
9153
+ workspaceRoot,
9154
+ agentDir,
9155
+ piPackages ?? []
9156
+ );
9157
+ const packageManager = new DefaultPackageManager({
9127
9158
  cwd: workspaceRoot,
9128
9159
  agentDir,
9129
- skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
9130
- includeDefaults: !noSkills
9160
+ settingsManager
9131
9161
  });
9132
- const skills = result.skills.map((s) => ({
9133
- name: s.name,
9134
- description: s.description
9135
- }));
9136
- cached.set(cacheKey, { skills, expiresAt: now + CACHE_TTL_MS2 });
9137
- return reply.code(200).send({ skills });
9162
+ const resolved = await packageManager.resolve();
9163
+ return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
9164
+ })();
9165
+ const result = loadSkills2({
9166
+ cwd: workspaceRoot,
9167
+ agentDir,
9168
+ skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
9169
+ includeDefaults: !noSkills
9170
+ });
9171
+ const skills = result.skills.map((s) => ({
9172
+ name: String(s.name),
9173
+ description: String(s.description ?? ""),
9174
+ ...typeof s.filePath === "string" ? { filePath: s.filePath } : {},
9175
+ ...typeof s.sourceInfo?.scope === "string" ? { source: s.sourceInfo.scope } : {}
9176
+ }));
9177
+ const entry = { skills, expiresAt: now + CACHE_TTL_MS2 };
9178
+ cached.set(cacheKey, entry);
9179
+ return entry;
9180
+ }
9181
+ app.get("/api/v1/agent/skills", async (request, reply) => {
9182
+ try {
9183
+ const entry = await resolveSkillsForRequest(request, request.query.refresh === "1");
9184
+ return reply.code(200).send({ skills: entry.skills });
9138
9185
  } catch (error) {
9139
9186
  const message = error instanceof Error ? error.message : String(error);
9140
9187
  request.log.warn({ err: error }, "[agent] failed to load skills");
@@ -12075,8 +12122,8 @@ async function promptImagesFromAttachments(attachments, workspace) {
12075
12122
  }
12076
12123
  if (!workspace?.readBinaryFile || !isWorkspaceImageAttachment(attachment)) continue;
12077
12124
  try {
12078
- const stat11 = await workspace.stat(attachment.path);
12079
- if (stat11.kind !== "file" || stat11.size <= 0 || stat11.size > MAX_PROMPT_IMAGE_BYTES) continue;
12125
+ const stat12 = await workspace.stat(attachment.path);
12126
+ if (stat12.kind !== "file" || stat12.size <= 0 || stat12.size > MAX_PROMPT_IMAGE_BYTES) continue;
12080
12127
  const bytes = await workspace.readBinaryFile(attachment.path);
12081
12128
  if (bytes.byteLength <= 0 || bytes.byteLength > MAX_PROMPT_IMAGE_BYTES) continue;
12082
12129
  const detectedMimeType = detectPromptImageMimeType(bytes);
@@ -12376,7 +12423,7 @@ function mergeTools(options) {
12376
12423
  }
12377
12424
 
12378
12425
  // src/server/tools/upload/index.ts
12379
- import { readFile as readFile12 } from "fs/promises";
12426
+ import { readFile as readFile13 } from "fs/promises";
12380
12427
  import { extname as extname5, join as join14 } from "path";
12381
12428
  var DEFAULT_UPLOAD_DIR = "assets/images";
12382
12429
  var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
@@ -12445,7 +12492,7 @@ function buildUploadAgentTools(bundle) {
12445
12492
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
12446
12493
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
12447
12494
  try {
12448
- const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(getRuntimeBundleStorageRoot(bundle), filePath));
12495
+ const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile13(join14(getRuntimeBundleStorageRoot(bundle), filePath));
12449
12496
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
12450
12497
  return {
12451
12498
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
@@ -74,7 +74,7 @@
74
74
  "use-stick-to-bottom": "^1.1.3",
75
75
  "yaml": "^2.8.3",
76
76
  "zod": "^3.25.76",
77
- "@hachej/boring-ui-kit": "0.1.58"
77
+ "@hachej/boring-ui-kit": "0.1.60"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@antithesishq/bombadil": "0.5.0",