@parallelworks/ui 0.18.0 → 0.19.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.
Files changed (161) hide show
  1. package/README.md +86 -6
  2. package/dist/ai/adapter/memoryStore.d.ts +8 -0
  3. package/dist/ai/adapter/mock/index.d.ts +2 -0
  4. package/dist/ai/adapter/mock/index.js +81 -0
  5. package/dist/ai/adapter/mock/mockAdapter.d.ts +4 -0
  6. package/dist/ai/adapter/openai/createOpenAIStream.d.ts +9 -0
  7. package/dist/ai/adapter/openai/errors.d.ts +3 -0
  8. package/dist/ai/adapter/openai/index.d.ts +6 -0
  9. package/dist/ai/adapter/openai/index.js +334 -0
  10. package/dist/ai/adapter/openai/openaiAdapter.d.ts +9 -0
  11. package/dist/ai/adapter/openai/responses.d.ts +4 -0
  12. package/dist/ai/adapter/openai/sse.d.ts +24 -0
  13. package/dist/ai/adapter/openai/wire.d.ts +40 -0
  14. package/dist/ai/adapter/types.d.ts +131 -0
  15. package/dist/ai/agent/classifier.d.ts +2 -0
  16. package/dist/ai/agent/diff.d.ts +35 -0
  17. package/dist/ai/agent/diffHighlight.d.ts +6 -0
  18. package/dist/ai/agent/index.d.ts +6 -0
  19. package/dist/ai/agent/permissions.d.ts +11 -0
  20. package/dist/ai/agent/toolfmt.d.ts +18 -0
  21. package/dist/ai/agent/transcript.d.ts +38 -0
  22. package/dist/ai/ai.css +325 -0
  23. package/dist/ai/components/AllocationSelector.d.ts +4 -0
  24. package/dist/ai/components/AttachmentManager.d.ts +1 -0
  25. package/dist/ai/components/AttachmentUpload.d.ts +24 -0
  26. package/dist/ai/components/BlockedGroupBanner.d.ts +1 -0
  27. package/dist/ai/components/BranchNavigator.d.ts +8 -0
  28. package/dist/ai/components/ChatEmptyState.d.ts +3 -0
  29. package/dist/ai/components/ChatInput.d.ts +45 -0
  30. package/dist/ai/components/ChatLayout.d.ts +5 -0
  31. package/dist/ai/components/ChatMessage.d.ts +24 -0
  32. package/dist/ai/components/ChatMessageList.d.ts +35 -0
  33. package/dist/ai/components/ChatSidebar.d.ts +5 -0
  34. package/dist/ai/components/ChatThread.d.ts +3 -0
  35. package/dist/ai/components/ComposerChrome.d.ts +18 -0
  36. package/dist/ai/components/ComposerFrame.d.ts +16 -0
  37. package/dist/ai/components/DragOverlay.d.ts +5 -0
  38. package/dist/ai/components/ModelSelector.d.ts +6 -0
  39. package/dist/ai/components/ShareDialog.d.ts +11 -0
  40. package/dist/ai/components/SlashMenu.d.ts +28 -0
  41. package/dist/ai/components/agent/ActivityLine.d.ts +21 -0
  42. package/dist/ai/components/agent/AgentMessageParts.d.ts +54 -0
  43. package/dist/ai/components/agent/PermissionPicker.d.ts +11 -0
  44. package/dist/ai/components/attachmentKeys.d.ts +4 -0
  45. package/dist/ai/core/ChatProvider.d.ts +66 -0
  46. package/dist/ai/core/KeyboardShortcutsProvider.d.ts +5 -0
  47. package/dist/ai/core/chatReducer.d.ts +114 -0
  48. package/dist/ai/core/config.d.ts +47 -0
  49. package/dist/ai/core/events.d.ts +1 -0
  50. package/dist/ai/core/exportConversation.d.ts +4 -0
  51. package/dist/ai/core/greeting.d.ts +3 -0
  52. package/dist/ai/core/parts.d.ts +11 -0
  53. package/dist/ai/core/useDragDrop.d.ts +14 -0
  54. package/dist/ai/core/useKeyboardShortcuts.d.ts +12 -0
  55. package/dist/ai/core/useSSEStream.d.ts +34 -0
  56. package/dist/ai/index.d.ts +40 -0
  57. package/dist/ai/index.js +6645 -0
  58. package/dist/ai/strings.d.ts +167 -0
  59. package/dist/ai/types.d.ts +236 -0
  60. package/dist/ai/ui/Avatar.d.ts +8 -0
  61. package/dist/ai/ui/Dropdown.d.ts +31 -0
  62. package/dist/ai/ui/Markdown.d.ts +8 -0
  63. package/dist/ai/ui/Notice.d.ts +21 -0
  64. package/dist/ai/ui/buttons.d.ts +1 -0
  65. package/dist/ai/ui/codeBreaks.d.ts +16 -0
  66. package/dist/ai/ui/prose.d.ts +1 -0
  67. package/dist/ai/ui/sidebar.d.ts +81 -0
  68. package/dist/ai/utils.d.ts +4 -0
  69. package/dist/assets/{editor.worker-CuBRIwi-.js → editor.worker-BmHUIVG4.js} +194 -163
  70. package/dist/assets/{json.worker-8ytS02vz.js → json.worker-D8bJ92nw.js} +384 -338
  71. package/dist/assets/{yaml.worker-DJC-VUcv.js → yaml.worker-C4gyGAko.js} +15657 -13823
  72. package/dist/chunks/{CodePreviewEditor-4lx3kE-w.js → CodePreviewEditor-YqQBz1YL.js} +1 -1
  73. package/dist/chunks/ConfirmModal-aOR4O1QE.js +79 -0
  74. package/dist/chunks/{FieldWrapper-Bef01EWO.js → FieldWrapper-Cszwg-4B.js} +3 -7
  75. package/dist/chunks/{CreateModal-BY6DLmur.js → Markdown-CM-FenNE.js} +19 -91
  76. package/dist/chunks/Monaco-DJ8MW-Dq.js +110 -0
  77. package/dist/chunks/{PageHeader-l49p1nM9.js → PageHeader-0kG_iypP.js} +67 -65
  78. package/dist/chunks/{Provider-Ghua9PkN.js → Provider-CDYurraE.js} +54 -20
  79. package/dist/chunks/{SwitchToggle-CHPP8uZQ.js → SwitchToggle-DShNdcao.js} +135 -132
  80. package/dist/chunks/{Table-4OHjdKdO.js → Table-DU2MW8xz.js} +5 -3
  81. package/dist/chunks/{Tooltip-Crxo_eU_.js → Tooltip-K5HJr4_p.js} +31 -4
  82. package/dist/chunks/duration-BOv8Phl8.js +17 -0
  83. package/dist/chunks/{editor-9_uVtUyy.js → editor-Dk1rbV75.js} +32 -18
  84. package/dist/chunks/focus-D_iZJTW1.js +7 -0
  85. package/dist/chunks/{list-qrojZZNv.js → list-CJm875m7.js} +377 -370
  86. package/dist/chunks/logviewer-BTKuQWzw.js +828 -0
  87. package/dist/chunks/memoryStore-CC91S5LP.js +152 -0
  88. package/dist/chunks/types-Yy0NrerF.js +11 -0
  89. package/dist/components/Avatar.d.ts +1 -1
  90. package/dist/components/Breadcrumbs.d.ts +1 -1
  91. package/dist/components/Dropdown.d.ts +1 -1
  92. package/dist/components/EmptyState.d.ts +1 -1
  93. package/dist/components/Indicator.d.ts +2 -2
  94. package/dist/components/Input.d.ts +1 -1
  95. package/dist/components/LetterBadge.d.ts +1 -1
  96. package/dist/components/MarkdownImpl.d.ts +1 -1
  97. package/dist/components/Provider.d.ts +16 -65
  98. package/dist/components/SettingRow.d.ts +1 -1
  99. package/dist/components/SettingsGroup.d.ts +1 -1
  100. package/dist/components/focus.d.ts +2 -0
  101. package/dist/components/keys.d.ts +12 -0
  102. package/dist/duration.d.ts +3 -0
  103. package/dist/editor/Monaco.d.ts +1 -20
  104. package/dist/editor/index.d.ts +2 -1
  105. package/dist/editor/index.js +2 -2
  106. package/dist/editor/yaml.d.ts +5 -0
  107. package/dist/engine.d.ts +98 -0
  108. package/dist/file-explorer/FileExplorer.d.ts +11 -2
  109. package/dist/file-explorer/FileExplorerContext.d.ts +1 -1
  110. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +1 -1
  111. package/dist/file-explorer/FilePreview/previewType.d.ts +1 -1
  112. package/dist/file-explorer/FileRow.d.ts +1 -1
  113. package/dist/file-explorer/helpers.d.ts +4 -4
  114. package/dist/file-explorer/index.d.ts +3 -3
  115. package/dist/file-explorer/index.js +613 -647
  116. package/dist/file-explorer/lib/types.d.ts +6 -1
  117. package/dist/file-explorer/lib/utils.d.ts +1 -3
  118. package/dist/file-explorer/useNearEnd.d.ts +1 -1
  119. package/dist/form/FieldWrapper.d.ts +2 -2
  120. package/dist/form/Fields/ResourceOptionIcon.d.ts +1 -1
  121. package/dist/form/Form.d.ts +4 -21
  122. package/dist/form/index.d.ts +11 -12
  123. package/dist/form/index.js +1488 -1611
  124. package/dist/form/lib.d.ts +6 -1
  125. package/dist/form/types/fieldComponentTypes.d.ts +3 -2
  126. package/dist/form/types/fieldTypes.d.ts +6 -20
  127. package/dist/form/useParsedOpts.d.ts +1 -1
  128. package/dist/form/utils/{slurmDuration.d.ts → duration.d.ts} +2 -2
  129. package/dist/{workflow → graph}/MatrixGroup.d.ts +2 -2
  130. package/dist/{workflow → graph}/index.d.ts +1 -1
  131. package/dist/{workflow → graph}/index.js +215 -180
  132. package/dist/{workflow → graph}/types.d.ts +3 -3
  133. package/dist/icons.d.ts +20 -21
  134. package/dist/index.d.ts +38 -40
  135. package/dist/index.js +347 -346
  136. package/dist/list/ListTable.d.ts +1 -1
  137. package/dist/list/ListViewControls.d.ts +2 -2
  138. package/dist/list/index.d.ts +9 -9
  139. package/dist/list/index.js +2 -2
  140. package/dist/logviewer/LogViewer.d.ts +1 -1
  141. package/dist/logviewer/index.d.ts +1 -0
  142. package/dist/logviewer/index.js +2 -742
  143. package/dist/logviewer/segments.d.ts +9 -0
  144. package/dist/styles/fonts.css +4 -4
  145. package/dist/styles/theme.css +6 -4
  146. package/dist/styles.css +1 -1
  147. package/dist/theme/index.js +2 -2
  148. package/package.json +38 -14
  149. package/LICENSE +0 -202
  150. package/dist/chunks/Monaco-C1POOdVv.js +0 -1046
  151. package/dist/editor/usesGitlab.d.ts +0 -97
  152. package/dist/form/Fields/BucketReadOnlyField.d.ts +0 -7
  153. package/dist/form/types/workflow-schema.d.ts +0 -1145
  154. package/dist/form/utils/pwResolver.d.ts +0 -18
  155. package/dist/chunks/{MarkdownImpl-Bq4QhrtA.js → MarkdownImpl-CyeTVvSt.js} +1 -1
  156. /package/dist/{workflow → graph}/AnnotationBanner.d.ts +0 -0
  157. /package/dist/{workflow → graph}/Collapse.d.ts +0 -0
  158. /package/dist/{workflow → graph}/DependencyGraph.d.ts +0 -0
  159. /package/dist/{workflow → graph}/JobSummary.d.ts +0 -0
  160. /package/dist/{workflow → graph}/Reveal.d.ts +0 -0
  161. /package/dist/{workflow → graph}/TreeView.d.ts +0 -0
package/README.md CHANGED
@@ -1,8 +1,24 @@
1
1
  # @parallelworks/ui
2
2
 
3
- Shared UI foundation for Parallel Works packages and applications: the unified
4
- theme contract, the icon barrel, and (in later versions) core primitives such
5
- as tables, tooltips, loaders, and modals.
3
+ React components built on one theme contract. Each surface is its own subpath,
4
+ so an application ships only what it imports:
5
+
6
+ | Subpath | Contents |
7
+ | --- | --- |
8
+ | `.` | theme contract, primitives (buttons, inputs, tables, tooltips, modals, dropdowns, settings layouts, page header), `UIProvider` |
9
+ | `./theme` | `deriveTheme`, presets, `applyTheme` |
10
+ | `./icons` | the icon barrel |
11
+ | `./list` | list pages, tables, row menus and list controls |
12
+ | `./form` | `DynamicForm` and its field registry |
13
+ | `./graph` | job dependency graph and tree view |
14
+ | `./editor` | Monaco editor (`monaco-editor` and `monaco-yaml` are optional peers) |
15
+ | `./logviewer` | log viewer with ANSI color and search |
16
+ | `./file-explorer` | object storage explorer over a host-supplied provider |
17
+ | `./ai`, `./ai/openai`, `./ai/mock` | chat interface and adapters |
18
+ | `./theme.css`, `./base.css`, `./fonts.css`, `./styles.css`, `./logviewer.css`, `./ai.css` | stylesheets |
19
+
20
+ Hosts bind notifications, strings, navigation, data hooks and the workflow
21
+ engine through `UIProvider`.
6
22
 
7
23
  ## Theme contract
8
24
 
@@ -35,9 +51,9 @@ import '@parallelworks/ui/styles.css'
35
51
  ```
36
52
 
37
53
  The prebuilt stylesheet nests all of its rules in the `pw-ui` cascade layer,
38
- so loading it alongside other Parallel Works package stylesheets (for example
39
- `@parallelworks/ai-chat/styles.css`, which uses the `pw-ai-chat` layer) is
40
- order-independent.
54
+ so its precedence does not depend on where it loads. It includes Tailwind's
55
+ preflight reset; a host with its own Tailwind build should use the `@source`
56
+ route instead to avoid a double preflight.
41
57
 
42
58
  ## Icons
43
59
 
@@ -47,3 +63,67 @@ import { AddIcon, LoaderIcon, TrashIcon } from '@parallelworks/ui/icons'
47
63
 
48
64
  The barrel re-exports a curated, stable set of icon names over `react-icons`
49
65
  so applications never depend on a specific icon pack directly.
66
+
67
+ ## Workflow engine
68
+
69
+ The workflow surfaces (`/form`'s `DynamicForm`, `/graph`'s `DependencyGraph`,
70
+ the editor's workflow completions, and the log viewer's workflow commands) do
71
+ not implement a workflow format themselves. They call a `WorkflowEngine` that
72
+ the host passes once:
73
+
74
+ ```tsx
75
+ import { UIProvider } from '@parallelworks/ui'
76
+
77
+ <UIProvider engine={engine}>{app}</UIProvider>
78
+ ```
79
+
80
+ The engine converts input definitions into the form's field tree, resolves
81
+ `${{ }}` expressions, and supplies the job graph's dependencies, matrix
82
+ grouping, step labels and log paths. The form and graph throw without one; the
83
+ editor and log viewer fall back to plain behavior.
84
+
85
+ ## AI chat
86
+
87
+ `@parallelworks/ui/ai` is a chat interface (thread, composer, streaming,
88
+ sidebar, model selector, attachments, sharing, branching) decoupled from any
89
+ backend through a `ChatAdapter`. State lives in `ChatProvider` and is read
90
+ with `useChat`:
91
+
92
+ ```tsx
93
+ import { ChatProvider, ChatLayout, ChatThread } from '@parallelworks/ui/ai'
94
+ import { createMockChatAdapter } from '@parallelworks/ui/ai/mock'
95
+
96
+ export function Chat() {
97
+ return (
98
+ <ChatProvider
99
+ adapter={createMockChatAdapter()}
100
+ currentUser={{ id: 'u1', username: 'you', name: 'You' }}
101
+ navigation={{ toConversation: id => {}, toNewChat: () => {}, toAttachments: () => {} }}
102
+ notify={{ success: console.log, error: console.error, info: console.log }}
103
+ >
104
+ <ChatLayout>
105
+ <ChatThread />
106
+ </ChatLayout>
107
+ </ChatProvider>
108
+ )
109
+ }
110
+ ```
111
+
112
+ Swap the mock for `createOpenAIChatAdapter({ baseUrl, apiKey })` from
113
+ `@parallelworks/ui/ai/openai`, or implement `ChatAdapter` against your own
114
+ backend. It requires `conversations`, `models` and `streamCompletion`; the
115
+ optional capability groups (`providers`, `attachments`, `sharing`,
116
+ `allocations`) hide their UI when absent. Adapter methods throw
117
+ `ChatAdapterError` with a user-facing `message`. Model entries and wire
118
+ messages keep OpenAI-compatible snake_case field names.
119
+
120
+ Tailwind v4 consumers add the chat rules (markdown rendering, streaming
121
+ animations) and streamdown's classes next to the theme contract:
122
+
123
+ ```css
124
+ @import 'streamdown/styles.css';
125
+ @import '@parallelworks/ui/ai.css';
126
+ @source '../node_modules/streamdown/dist/*.js';
127
+ ```
128
+
129
+ The prebuilt `@parallelworks/ui/styles.css` already includes both.
@@ -0,0 +1,8 @@
1
+ import { type ConversationsAdapter, type StreamCompletion } from './types.js';
2
+ export interface MemoryConversationStore {
3
+ conversations: ConversationsAdapter;
4
+ recordingStream(inner: StreamCompletion): StreamCompletion;
5
+ }
6
+ export declare function createMemoryConversationStore(options?: {
7
+ storageKey?: string;
8
+ }): MemoryConversationStore;
@@ -0,0 +1,2 @@
1
+ export { createMemoryConversationStore, type MemoryConversationStore, } from '../memoryStore.js';
2
+ export { createMockChatAdapter } from './mockAdapter.js';
@@ -0,0 +1,81 @@
1
+ import { t as createMemoryConversationStore } from "../../../chunks/memoryStore-CC91S5LP.js";
2
+ //#region src/ai/adapter/mock/mockAdapter.ts
3
+ var CANNED_REPLIES = [
4
+ "Here's a canned reply from the mock adapter. It streams word by word so the typewriter animation, cancel behavior, and background streams all exercise the same code paths as a real backend.\n\n- No network requests are made\n- Conversations persist in localStorage\n- Try cancelling mid-stream: the partial reply is kept",
5
+ "Everything you see is generated locally. The mock adapter implements the same `ChatAdapter` interface as the OpenAI-compatible adapter and any backend's own, so the UI can't tell the difference.\n\n```ts\nconst adapter = createMockChatAdapter()\n```\n\nMarkdown, `inline code`, and code blocks render through the same pipeline.",
6
+ "Branching works too: edit one of your earlier messages or hit regenerate on a reply, then use the arrows to flip between branches."
7
+ ];
8
+ var THINKING_PREAMBLE = "Thinking about the question, weighing a few canned options, and picking the next reply in the rotation...";
9
+ function sleep(ms, signal) {
10
+ return new Promise((resolve, reject) => {
11
+ if (signal?.aborted) {
12
+ reject(new DOMException("The operation was aborted.", "AbortError"));
13
+ return;
14
+ }
15
+ const onAbort = () => {
16
+ clearTimeout(timer);
17
+ reject(new DOMException("The operation was aborted.", "AbortError"));
18
+ };
19
+ const timer = setTimeout(() => {
20
+ signal?.removeEventListener("abort", onAbort);
21
+ resolve();
22
+ }, ms);
23
+ signal?.addEventListener("abort", onAbort, { once: true });
24
+ });
25
+ }
26
+ function createMockChatAdapter(options) {
27
+ const store = createMemoryConversationStore({ storageKey: options?.storageKey ?? "ai-chat-mock-conversations" });
28
+ let replyIndex = 0;
29
+ return {
30
+ conversations: store.conversations,
31
+ models: { async list() {
32
+ return {
33
+ models: [{
34
+ id: "mock:local/canned-small",
35
+ object: "model",
36
+ created: 0,
37
+ owned_by: "mock",
38
+ provider: "Mock Provider",
39
+ tool_calling_mode: "none"
40
+ }, {
41
+ id: "mock:local/canned-thinking",
42
+ object: "model",
43
+ created: 0,
44
+ owned_by: "mock",
45
+ provider: "Mock Provider",
46
+ tool_calling_mode: "none"
47
+ }],
48
+ unreachableSessions: []
49
+ };
50
+ } },
51
+ streamCompletion: store.recordingStream(async (req, handlers, signal) => {
52
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
53
+ let reasoning = "";
54
+ if (req.model.includes("thinking")) for (const word of THINKING_PREAMBLE.split(/(?<= )/)) {
55
+ await sleep(40, signal);
56
+ reasoning += word;
57
+ handlers.onReasoning?.(word);
58
+ }
59
+ const reply = CANNED_REPLIES[replyIndex % CANNED_REPLIES.length] ?? "";
60
+ replyIndex++;
61
+ await sleep(250, signal);
62
+ let content = "";
63
+ for (const word of reply.split(/(?<= )/)) {
64
+ await sleep(25, signal);
65
+ content += word;
66
+ handlers.onContent(word);
67
+ }
68
+ return {
69
+ content,
70
+ messageId: "",
71
+ toolCalls: [],
72
+ finishReason: "stop",
73
+ model: req.model,
74
+ reasoning,
75
+ responsesOutput: []
76
+ };
77
+ })
78
+ };
79
+ }
80
+ //#endregion
81
+ export { createMemoryConversationStore, createMockChatAdapter };
@@ -0,0 +1,4 @@
1
+ import type { ChatAdapter } from '../types.js';
2
+ export declare function createMockChatAdapter(options?: {
3
+ storageKey?: string;
4
+ }): ChatAdapter;
@@ -0,0 +1,9 @@
1
+ import { type StreamCompletion, type StreamRequest } from '../types.js';
2
+ export interface OpenAIStreamConfig {
3
+ completionsUrl: string;
4
+ responsesUrl?: string;
5
+ buildHeaders?: (req: StreamRequest) => Record<string, string>;
6
+ credentials?: RequestCredentials;
7
+ fetch?: typeof globalThis.fetch;
8
+ }
9
+ export declare function createOpenAICompatibleStream(config: OpenAIStreamConfig): StreamCompletion;
@@ -0,0 +1,3 @@
1
+ export declare function apiErrorMessage(errorData: unknown, fallback: string): string;
2
+ export declare function statusFallbackMessage(status: number): string;
3
+ export declare function errorMessageFromResponse(res: Response): Promise<string>;
@@ -0,0 +1,6 @@
1
+ export { createOpenAICompatibleStream, type OpenAIStreamConfig, } from './createOpenAIStream.js';
2
+ export { apiErrorMessage, errorMessageFromResponse, statusFallbackMessage, } from './errors.js';
3
+ export { createOpenAIChatAdapter, type OpenAIChatAdapterOptions, } from './openaiAdapter.js';
4
+ export { buildResponsesRequestBody, parseResponsesSSELine, } from './responses.js';
5
+ export { type ParsedSSELine, parseSSELine, processSSEStream, type SSELineParser, } from './sse.js';
6
+ export type { CompletionChunk, CompletionChunkChoice, CompletionMessage, CompletionRequest, ModelsWireResponse, } from './wire.js';
@@ -0,0 +1,334 @@
1
+ import { t as ChatAdapterError } from "../../../chunks/types-Yy0NrerF.js";
2
+ import { t as createMemoryConversationStore } from "../../../chunks/memoryStore-CC91S5LP.js";
3
+ //#region src/ai/adapter/openai/errors.ts
4
+ function apiErrorMessage(errorData, fallback) {
5
+ if (typeof errorData !== "object" || errorData === null) return fallback;
6
+ const data = errorData;
7
+ const candidate = data["message"] ?? data["error"] ?? data["detail"];
8
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
9
+ if (candidate && typeof candidate === "object") {
10
+ const nested = candidate["message"];
11
+ if (typeof nested === "string" && nested.trim()) return nested;
12
+ try {
13
+ return JSON.stringify(candidate);
14
+ } catch {
15
+ return fallback;
16
+ }
17
+ }
18
+ return fallback;
19
+ }
20
+ function statusFallbackMessage(status) {
21
+ if (status === 401) return "Your session has expired. Please refresh the page and try again.";
22
+ if (status === 403) return "You do not have permission to use this AI provider.";
23
+ if (status === 404) return "The AI provider or model was not found. Please select a different provider.";
24
+ if (status === 429) return "Too many requests. Please wait a moment and try again.";
25
+ if (status >= 500) return "The AI service is temporarily unavailable. Please try again later.";
26
+ return "Failed to send message";
27
+ }
28
+ async function errorMessageFromResponse(res) {
29
+ let message = statusFallbackMessage(res.status);
30
+ try {
31
+ const contentType = res.headers.get("content-type");
32
+ const text = await res.text();
33
+ if (contentType?.includes("application/json") && text) message = apiErrorMessage(JSON.parse(text), message);
34
+ else if (text && res.status < 500) message = text.substring(0, 200);
35
+ } catch {}
36
+ return message;
37
+ }
38
+ //#endregion
39
+ //#region src/ai/adapter/openai/responses.ts
40
+ function contentString(content) {
41
+ return typeof content === "string" ? content : "";
42
+ }
43
+ function buildResponsesRequestBody(model, messages) {
44
+ const input = [];
45
+ let attachmentIds;
46
+ for (const m of messages) switch (m.role) {
47
+ case "system":
48
+ input.push({
49
+ type: "message",
50
+ role: "system",
51
+ content: contentString(m.content)
52
+ });
53
+ break;
54
+ case "user":
55
+ if (m.attachment_ids?.length) attachmentIds = m.attachment_ids;
56
+ input.push({
57
+ type: "message",
58
+ role: "user",
59
+ content: contentString(m.content)
60
+ });
61
+ break;
62
+ case "assistant": {
63
+ const replayed = Array.isArray(m.responses_output) ? replayableOutput(m.responses_output) : [];
64
+ if (replayed.length > 0) {
65
+ for (const item of replayed) input.push(item);
66
+ break;
67
+ }
68
+ const text = contentString(m.content);
69
+ const toolCalls = m.tool_calls ?? [];
70
+ if (text || toolCalls.length === 0) input.push({
71
+ type: "message",
72
+ role: "assistant",
73
+ content: text
74
+ });
75
+ for (const tc of toolCalls) input.push({
76
+ type: "function_call",
77
+ call_id: tc.id ?? "",
78
+ name: tc.function.name,
79
+ arguments: tc.function.arguments
80
+ });
81
+ break;
82
+ }
83
+ case "tool": input.push({
84
+ type: "function_call_output",
85
+ call_id: m.tool_call_id ?? "",
86
+ output: contentString(m.content)
87
+ });
88
+ }
89
+ const body = {
90
+ model,
91
+ input,
92
+ stream: true,
93
+ store: false,
94
+ include: ["reasoning.encrypted_content"]
95
+ };
96
+ if (attachmentIds?.length) body["attachment_ids"] = attachmentIds;
97
+ return body;
98
+ }
99
+ var REPLAYABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
100
+ "reasoning",
101
+ "message",
102
+ "function_call"
103
+ ]);
104
+ function replayableOutput(output) {
105
+ if (!output?.length) return [];
106
+ return output.filter((item) => {
107
+ if (typeof item !== "object" || item === null) return false;
108
+ const itemType = item.type;
109
+ return typeof itemType === "string" && REPLAYABLE_ITEM_TYPES.has(itemType);
110
+ });
111
+ }
112
+ function parseResponsesSSELine(line) {
113
+ if (!line.startsWith("data: ")) return null;
114
+ const data = line.slice(6);
115
+ if (data === "[DONE]" || data === "\"[DONE]\"") return null;
116
+ let event;
117
+ try {
118
+ event = JSON.parse(data);
119
+ } catch {
120
+ return null;
121
+ }
122
+ if (event.error) return { error: event.error.message || "Unknown error" };
123
+ const model = event.response?.model;
124
+ switch (event.type) {
125
+ case "pw.message_id": return event.messageId ? { messageId: event.messageId } : null;
126
+ case "response.output_text.delta":
127
+ case "response.refusal.delta": return typeof event.delta === "string" ? { content: event.delta } : null;
128
+ case "response.reasoning_summary_text.delta": return typeof event.delta === "string" ? { reasoningContent: event.delta } : null;
129
+ case "response.created": return model ? { model } : null;
130
+ case "response.completed": return {
131
+ ...model && { model },
132
+ finishReason: "stop",
133
+ responsesOutput: replayableOutput(event.response?.output)
134
+ };
135
+ case "response.incomplete": return {
136
+ ...model && { model },
137
+ finishReason: "length",
138
+ responsesOutput: replayableOutput(event.response?.output)
139
+ };
140
+ case "response.failed": return { error: event.response?.error?.message || "Response failed" };
141
+ case "error": return { error: event.message || "Stream error" };
142
+ default: return null;
143
+ }
144
+ }
145
+ //#endregion
146
+ //#region src/ai/adapter/openai/sse.ts
147
+ function parseSSELine(line) {
148
+ if (!line.startsWith("data: ")) return null;
149
+ const data = line.slice(6);
150
+ if (data === "[DONE]" || data === "\"[DONE]\"") return null;
151
+ try {
152
+ const parsed = JSON.parse(data);
153
+ if (parsed.error) return { error: parsed.error.message || "Unknown error" };
154
+ const chunk = parsed;
155
+ const choice = chunk.choices?.[0];
156
+ return {
157
+ id: chunk.id,
158
+ messageId: chunk.messageId,
159
+ content: choice?.delta?.content,
160
+ reasoningContent: choice?.delta?.reasoning_content,
161
+ toolCalls: choice?.delta?.tool_calls ?? void 0,
162
+ finishReason: choice?.finish_reason,
163
+ model: chunk.model
164
+ };
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ /** Process an SSE stream from a ReadableStream reader */
170
+ async function processSSEStream(reader, onContent, onToolCall, onError, onReasoning, onMessageId, parseLine = parseSSELine) {
171
+ const decoder = new TextDecoder();
172
+ let assistantContent = "";
173
+ let assistantReasoning = "";
174
+ let assistantMessageId = "";
175
+ let finishReason = null;
176
+ let resolvedModel = null;
177
+ let responsesOutput = [];
178
+ const toolCallsMap = /* @__PURE__ */ new Map();
179
+ let lineBuffer = "";
180
+ while (true) {
181
+ const result = await reader.read();
182
+ if (result.done) break;
183
+ const chunk = decoder.decode(result.value, { stream: true });
184
+ const lines = (lineBuffer + chunk).split("\n");
185
+ lineBuffer = lines.pop() || "";
186
+ for (const line of lines) {
187
+ const parsed = parseLine(line);
188
+ if (parsed) {
189
+ if (parsed.error) {
190
+ onError?.(parsed.error);
191
+ throw new Error(parsed.error);
192
+ }
193
+ if (parsed.messageId && !assistantMessageId) {
194
+ assistantMessageId = parsed.messageId;
195
+ onMessageId?.(parsed.messageId);
196
+ } else if (parsed.id && !assistantMessageId) assistantMessageId = parsed.id;
197
+ if (parsed.model && !resolvedModel) resolvedModel = parsed.model;
198
+ if (parsed.content) {
199
+ assistantContent += parsed.content;
200
+ onContent(parsed.content);
201
+ }
202
+ if (parsed.reasoningContent) {
203
+ assistantReasoning += parsed.reasoningContent;
204
+ onReasoning?.(parsed.reasoningContent);
205
+ }
206
+ if (parsed.finishReason) finishReason = parsed.finishReason;
207
+ if (parsed.responsesOutput?.length) responsesOutput = parsed.responsesOutput;
208
+ if (parsed.toolCalls) for (const tc of parsed.toolCalls) {
209
+ if (tc.index === void 0) continue;
210
+ const existing = toolCallsMap.get(tc.index);
211
+ if (existing) {
212
+ if (tc.function?.arguments) existing.arguments += tc.function.arguments;
213
+ } else toolCallsMap.set(tc.index, {
214
+ id: tc.id || "",
215
+ type: tc.type || "function",
216
+ name: tc.function?.name || "",
217
+ arguments: tc.function?.arguments || ""
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ if (lineBuffer.trim()) {
224
+ const parsed = parseLine(lineBuffer);
225
+ if (parsed) {
226
+ if (parsed.content) {
227
+ assistantContent += parsed.content;
228
+ onContent(parsed.content);
229
+ }
230
+ if (parsed.reasoningContent) {
231
+ assistantReasoning += parsed.reasoningContent;
232
+ onReasoning?.(parsed.reasoningContent);
233
+ }
234
+ if (parsed.finishReason) finishReason = parsed.finishReason;
235
+ }
236
+ }
237
+ const toolCalls = Array.from(toolCallsMap.values()).map((tc) => ({
238
+ id: tc.id,
239
+ type: tc.type,
240
+ function: {
241
+ name: tc.name,
242
+ arguments: tc.arguments
243
+ }
244
+ }));
245
+ if (toolCalls.length > 0 && onToolCall) onToolCall(toolCalls);
246
+ return {
247
+ content: assistantContent,
248
+ messageId: assistantMessageId,
249
+ toolCalls,
250
+ finishReason,
251
+ model: resolvedModel,
252
+ reasoning: assistantReasoning,
253
+ responsesOutput
254
+ };
255
+ }
256
+ //#endregion
257
+ //#region src/ai/adapter/openai/createOpenAIStream.ts
258
+ function createOpenAICompatibleStream(config) {
259
+ return async (req, handlers, signal) => {
260
+ const useResponses = req.useResponses === true && !!config.responsesUrl;
261
+ const url = useResponses ? config.responsesUrl : config.completionsUrl;
262
+ const body = useResponses ? buildResponsesRequestBody(req.model, req.messages) : {
263
+ model: req.model,
264
+ messages: req.messages,
265
+ stream: true
266
+ };
267
+ const res = await (config.fetch ?? globalThis.fetch.bind(globalThis))(url, {
268
+ method: "POST",
269
+ headers: {
270
+ "Content-Type": "application/json",
271
+ Accept: "text/event-stream",
272
+ ...config.buildHeaders?.(req)
273
+ },
274
+ ...config.credentials ? { credentials: config.credentials } : {},
275
+ body: JSON.stringify(body),
276
+ ...signal ? { signal } : {}
277
+ });
278
+ if (!res.ok) throw new ChatAdapterError(await errorMessageFromResponse(res), { status: res.status });
279
+ const reader = res.body?.getReader();
280
+ if (!reader) throw new ChatAdapterError("No response body");
281
+ return processSSEStream(reader, handlers.onContent, void 0, void 0, handlers.onReasoning, handlers.onMessageId, useResponses ? parseResponsesSSELine : void 0);
282
+ };
283
+ }
284
+ //#endregion
285
+ //#region src/ai/adapter/openai/openaiAdapter.ts
286
+ function providerLabelFor(baseUrl) {
287
+ try {
288
+ return new URL(baseUrl).host;
289
+ } catch {
290
+ return "OpenAI compatible";
291
+ }
292
+ }
293
+ function staticModel(id, provider) {
294
+ return {
295
+ id,
296
+ object: "model",
297
+ created: 0,
298
+ owned_by: "openai-compatible",
299
+ provider,
300
+ tool_calling_mode: "none"
301
+ };
302
+ }
303
+ function createOpenAIChatAdapter(options) {
304
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
305
+ const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
306
+ const headers = options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {};
307
+ const store = createMemoryConversationStore({ storageKey: options.storageKey ?? "ai-chat-openai-conversations" });
308
+ return {
309
+ conversations: store.conversations,
310
+ models: { async list() {
311
+ const providerLabel = providerLabelFor(baseUrl);
312
+ if (options.models?.length) return {
313
+ models: options.models.map((id) => staticModel(id, providerLabel)),
314
+ unreachableSessions: []
315
+ };
316
+ const res = await doFetch(`${baseUrl}/models`, { headers });
317
+ if (!res.ok) throw new ChatAdapterError(await errorMessageFromResponse(res), { status: res.status });
318
+ return {
319
+ models: ((await res.json()).data ?? []).map((m) => ({
320
+ ...staticModel(m.id, providerLabel),
321
+ ...m
322
+ })),
323
+ unreachableSessions: []
324
+ };
325
+ } },
326
+ streamCompletion: store.recordingStream(createOpenAICompatibleStream({
327
+ completionsUrl: `${baseUrl}/chat/completions`,
328
+ buildHeaders: () => headers,
329
+ ...options.fetch ? { fetch: options.fetch } : {}
330
+ }))
331
+ };
332
+ }
333
+ //#endregion
334
+ export { apiErrorMessage, buildResponsesRequestBody, createOpenAIChatAdapter, createOpenAICompatibleStream, errorMessageFromResponse, parseResponsesSSELine, parseSSELine, processSSEStream, statusFallbackMessage };
@@ -0,0 +1,9 @@
1
+ import { type ChatAdapter } from '../types.js';
2
+ export interface OpenAIChatAdapterOptions {
3
+ baseUrl: string;
4
+ apiKey?: string;
5
+ models?: string[];
6
+ storageKey?: string;
7
+ fetch?: typeof globalThis.fetch;
8
+ }
9
+ export declare function createOpenAIChatAdapter(options: OpenAIChatAdapterOptions): ChatAdapter;
@@ -0,0 +1,4 @@
1
+ import type { ParsedSSELine } from './sse.js';
2
+ import type { CompletionMessage } from './wire.js';
3
+ export declare function buildResponsesRequestBody(model: string, messages: CompletionMessage[]): Record<string, unknown>;
4
+ export declare function parseResponsesSSELine(line: string): ParsedSSELine;
@@ -0,0 +1,24 @@
1
+ import type { ToolCall, ToolCallDelta } from '../../types.js';
2
+ export type ParsedSSELine = {
3
+ id?: string | undefined;
4
+ messageId?: string | undefined;
5
+ content?: string | undefined;
6
+ reasoningContent?: string | undefined;
7
+ toolCalls?: ToolCallDelta[] | undefined;
8
+ finishReason?: string | null | undefined;
9
+ model?: string | undefined;
10
+ error?: string | undefined;
11
+ responsesOutput?: unknown[] | undefined;
12
+ } | null;
13
+ export type SSELineParser = (line: string) => ParsedSSELine;
14
+ export declare function parseSSELine(line: string): ParsedSSELine;
15
+ /** Process an SSE stream from a ReadableStream reader */
16
+ export declare function processSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>, onContent: (content: string) => void, onToolCall?: (toolCalls: ToolCall[]) => void, onError?: (error: string) => void, onReasoning?: (reasoning: string) => void, onMessageId?: (messageId: string) => void, parseLine?: SSELineParser): Promise<{
17
+ content: string;
18
+ messageId: string;
19
+ toolCalls: ToolCall[];
20
+ finishReason: string | null;
21
+ model: string | null;
22
+ reasoning: string;
23
+ responsesOutput: unknown[];
24
+ }>;
@@ -0,0 +1,40 @@
1
+ import type { ChatModel, TokenUsage, ToolCall, ToolCallDelta, UnreachableSession } from '../../types.js';
2
+ export interface CompletionMessage {
3
+ role: string;
4
+ content?: unknown;
5
+ name?: string;
6
+ reasoning_content?: string;
7
+ responses_output?: unknown[] | null;
8
+ tool_call_id?: string;
9
+ tool_calls?: ToolCall[] | null;
10
+ attachment_ids?: string[] | null | undefined;
11
+ }
12
+ export interface CompletionRequest {
13
+ model: string;
14
+ messages: CompletionMessage[];
15
+ stream?: boolean;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface CompletionChunkChoice {
19
+ delta?: {
20
+ content?: unknown;
21
+ reasoning_content?: string;
22
+ tool_calls?: ToolCallDelta[] | null;
23
+ };
24
+ finish_reason?: string | null;
25
+ index?: number;
26
+ }
27
+ export interface CompletionChunk {
28
+ id: string;
29
+ model: string;
30
+ object?: string;
31
+ created?: number;
32
+ messageId?: string;
33
+ choices?: CompletionChunkChoice[] | null;
34
+ usage?: TokenUsage;
35
+ }
36
+ export interface ModelsWireResponse {
37
+ object: string;
38
+ data: ChatModel[] | null;
39
+ unreachable_sessions?: UnreachableSession[] | null;
40
+ }