ompchamber 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -147
- package/dist/client/index.html +4 -2
- package/dist/client/static/css/index.5f630421b7.css +2 -0
- package/dist/client/static/js/77.0d81c74cf0.js +139 -0
- package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
- package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
- package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
- package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
- package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
- package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
- package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
- package/dist/client/static/js/async/e.93214d622b.js +79 -0
- package/dist/client/static/js/async/g.74ff36419f.js +2 -0
- package/dist/client/static/js/async/{m.2e1ea3db9e.js.LICENSE.txt → g.74ff36419f.js.LICENSE.txt} +5 -0
- package/dist/client/static/js/async/h1.393f066d09.js +1 -0
- package/dist/client/static/js/async/jl.874d352278.js +1 -0
- package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
- package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
- package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
- package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
- package/dist/client/static/js/async/px.fc6877924b.js +1 -0
- package/dist/client/static/js/async/q.33d6779f98.js +21 -0
- package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
- package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
- package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
- package/dist/client/static/js/async/vt.42416b707d.js +1 -0
- package/dist/client/static/js/async/x1.da8687159b.js +90 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
- package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.d99d829136.js +1 -0
- package/dist/client/static/js/index.c05535dbde.js +38 -0
- package/package.json +3 -3
- package/src/client/components/common/ComposerTextarea.tsx +9 -1
- package/src/client/components/common/MarkdownRenderer.tsx +50 -12
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
- package/src/client/components/settings/Modal.tsx +11 -2
- package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
- package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
- package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
- package/src/client/components/workspace/SearchPanel.tsx +16 -3
- package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
- package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
- package/src/client/components/workspace/btw-panel/index.tsx +129 -45
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
- package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
- package/src/client/components/workspace/editor/index.tsx +11 -2
- package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
- package/src/client/components/workspace/file-explorer/index.tsx +86 -16
- package/src/client/components/workspace/git-panel/index.tsx +60 -78
- package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
- package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
- package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
- package/src/client/data/theme/terminal.ts +50 -52
- package/src/client/hooks/chat/btw/index.ts +139 -18
- package/src/client/hooks/chat/btw/intercept.ts +8 -8
- package/src/client/hooks/chat/btw/mode.ts +111 -16
- package/src/client/hooks/chat/composer/index.ts +10 -4
- package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
- package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
- package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
- package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
- package/src/client/hooks/chat/timeline/index.ts +8 -20
- package/src/client/hooks/chat/timeline/queue.ts +66 -9
- package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
- package/src/client/hooks/chat/timeline/scroll.ts +93 -20
- package/src/client/hooks/chat/timeline/session-load.ts +7 -1
- package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
- package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
- package/src/client/hooks/settings/providers.ts +34 -3
- package/src/client/hooks/ui/open-file.ts +7 -1
- package/src/client/hooks/ui/theme.ts +82 -57
- package/src/client/hooks/workspace/file-tabs.ts +5 -1
- package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
- package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
- package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
- package/src/client/hooks/workspace/search-stream.ts +24 -5
- package/src/client/main.tsx +6 -0
- package/src/client/tailwind.css +19 -56
- package/src/server/lib/btw/child.server.ts +41 -0
- package/src/server/lib/btw/lifecycle.server.ts +254 -0
- package/src/server/lib/btw/prompt.ts +13 -5
- package/src/server/lib/btw/registry.server.ts +13 -4
- package/src/server/lib/btw/runtime.server.ts +149 -156
- package/src/server/lib/btw/schema.server.ts +27 -0
- package/src/server/lib/btw/service.server.ts +106 -5
- package/src/server/lib/btw/store.server.ts +57 -6
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/settings-store.test.ts +65 -0
- package/src/server/lib/db/settings-store.ts +25 -0
- package/src/server/lib/fs/atomic-write.ts +42 -4
- package/src/server/lib/fs/config-lock.ts +74 -0
- package/src/server/lib/fs/git-ignore.ts +55 -5
- package/src/server/lib/fs/ripgrep.ts +119 -0
- package/src/server/lib/lifecycle/identity.ts +10 -64
- package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
- package/src/server/lib/lifecycle/proc/index.ts +38 -0
- package/src/server/lib/lifecycle/proc/linux.ts +80 -0
- package/src/server/lib/lifecycle/proc/ps.ts +86 -0
- package/src/server/lib/lifecycle/proc/types.ts +33 -0
- package/src/server/lib/lifecycle/proc/win32.ts +53 -0
- package/src/server/lib/models/provider-registry.server.ts +2 -2
- package/src/server/lib/models/registry.server.ts +1 -1
- package/src/server/lib/omp/config/behavior.ts +17 -17
- package/src/server/lib/omp/config/disabled-providers.ts +84 -35
- package/src/server/lib/omp/config/document.ts +128 -0
- package/src/server/lib/omp/config/extensions.ts +11 -10
- package/src/server/lib/omp/config/mcp.ts +4 -57
- package/src/server/lib/omp/config/model-overrides.ts +123 -0
- package/src/server/lib/omp/config/models-config.ts +92 -0
- package/src/server/lib/omp/config/models-validation.ts +159 -0
- package/src/server/lib/omp/config/path-scoped.ts +74 -0
- package/src/server/lib/omp/config/providers.ts +241 -211
- package/src/server/lib/omp/config/roles.ts +6 -14
- package/src/server/lib/omp/core/auth-credentials.ts +1 -1
- package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
- package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
- package/src/server/lib/omp/rpc/manager.ts +21 -21
- package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
- package/src/server/lib/omp/rpc/session-commands.ts +17 -29
- package/src/server/lib/omp/rpc/session-registry.ts +5 -66
- package/src/server/lib/omp/rpc/web-state.ts +0 -6
- package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
- package/src/server/lib/omp/session/auto-title.server.ts +95 -20
- package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
- package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
- package/src/server/lib/queue/delivery-harness.ts +88 -0
- package/src/server/lib/queue/delivery.server.test.ts +78 -149
- package/src/server/lib/queue/delivery.server.ts +105 -13
- package/src/server/lib/queue/store.server.ts +30 -5
- package/src/server/lib/terminal/foreground.test.ts +164 -0
- package/src/server/lib/terminal/foreground.ts +34 -6
- package/src/server/lib/terminal/history.ts +8 -7
- package/src/server/plugins/compress.ts +33 -5
- package/src/server/plugins/ssr.ts +14 -7
- package/src/server/routes/btw/topics.ts +89 -2
- package/src/server/routes/fs/git.ts +5 -6
- package/src/server/routes/fs/search.ts +17 -26
- package/src/server/routes/sessions/queue.ts +17 -4
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/model-override.ts +69 -0
- package/src/server/routes/settings/provider-models.ts +35 -12
- package/src/server/routes/settings/providers.ts +31 -1
- package/src/server/routes/settings/root.ts +8 -3
- package/src/shared/lib/chat/btw/client.ts +34 -0
- package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
- package/src/shared/lib/code/shiki-themes.ts +9 -5
- package/src/shared/lib/fs/file-tree.ts +17 -12
- package/src/shared/lib/markdown/autolink.test.ts +32 -0
- package/src/shared/lib/markdown/autolink.ts +50 -0
- package/src/shared/lib/markdown/document-urls.test.ts +68 -0
- package/src/shared/lib/markdown/document-urls.ts +118 -0
- package/src/shared/lib/markdown/marked.ts +27 -15
- package/src/shared/lib/markdown/mermaid.ts +2 -2
- package/src/shared/lib/models/catalog.ts +1 -1
- package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
- package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
- package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
- package/src/shared/lib/settings/client.ts +10 -4
- package/src/shared/lib/settings/diff.test.ts +31 -0
- package/src/shared/lib/settings/diff.ts +37 -0
- package/src/shared/lib/theme/catalog.test.ts +168 -0
- package/src/shared/lib/theme/catalog.ts +98 -0
- package/src/shared/lib/theme/color.ts +88 -0
- package/src/shared/lib/theme/css.ts +70 -0
- package/src/shared/lib/theme/palettes/chamber.ts +61 -0
- package/src/shared/lib/theme/palettes/dark.ts +313 -0
- package/src/shared/lib/theme/palettes/light.ts +313 -0
- package/src/shared/types/btw.ts +83 -4
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +8 -1
- package/src/shared/types/settings/state.ts +10 -4
- package/src/shared/types/theme.ts +48 -0
- package/dist/client/static/css/index.b938f4a693.css +0 -2
- package/dist/client/static/js/77.8022a800b5.js +0 -139
- package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
- package/dist/client/static/js/async/66.86544ad741.js +0 -93
- package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
- package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
- package/dist/client/static/js/async/e.6443e739a2.js +0 -79
- package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
- package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
- package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
- package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
- package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
- package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
- package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
- package/dist/client/static/js/async/vt.20406a5604.js +0 -1
- package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
- package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
- package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
- package/dist/client/static/js/index.57b39ffa7f.js +0 -34
- package/src/client/components/workspace/btw-panel/Composer.tsx +0 -181
- package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
- /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.33d6779f98.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.57b39ffa7f.js.LICENSE.txt → index.c05535dbde.js.LICENSE.txt} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"@fontsource/fira-code": "^5.3.0",
|
|
50
50
|
"@shikijs/langs": "^4.4.3",
|
|
51
51
|
"@shikijs/themes": "^4.4.3",
|
|
52
|
-
"@tailwindcss/typography": "^0.5.20",
|
|
53
52
|
"@xterm/addon-fit": "^0.11.0",
|
|
54
53
|
"@xterm/addon-webgl": "^0.19.0",
|
|
55
54
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -63,7 +62,8 @@
|
|
|
63
62
|
"preact": "^10.29.8",
|
|
64
63
|
"remend": "^1.3.1",
|
|
65
64
|
"ripgrep": "^0.3.1",
|
|
66
|
-
"shiki": "^4.4.3"
|
|
65
|
+
"shiki": "^4.4.3",
|
|
66
|
+
"yaml": "^2.9.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@rsbuild/core": "^2.2.7",
|
|
@@ -19,6 +19,13 @@ export interface ComposerTextareaProps {
|
|
|
19
19
|
className?: string;
|
|
20
20
|
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void;
|
|
21
21
|
rootPath?: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the `@` / `/` / `!` / `#` autocomplete runs. Off for a composer with
|
|
24
|
+
* nothing to complete against — the side-question form has no file tree, no
|
|
25
|
+
* commands and no skills, and an `@` mention left in its text would reach the
|
|
26
|
+
* model as a literal no prompt builder translates.
|
|
27
|
+
*/
|
|
28
|
+
enablePicker?: boolean;
|
|
22
29
|
/**
|
|
23
30
|
* `mobile` makes a bare Enter insert a newline instead of sending: phone
|
|
24
31
|
* keyboards have no Shift key, so the configured Enter-to-send binding would
|
|
@@ -39,12 +46,13 @@ export function ComposerTextarea({
|
|
|
39
46
|
className,
|
|
40
47
|
onPaste,
|
|
41
48
|
rootPath,
|
|
49
|
+
enablePicker = true,
|
|
42
50
|
variant = 'desktop',
|
|
43
51
|
}: ComposerTextareaProps): ReactElement {
|
|
44
52
|
const isMobile = variant === 'mobile';
|
|
45
53
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
46
54
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
47
|
-
const composer = useComposerTrigger({ value, setValue: onChange, textareaRef, disabled, rootPath });
|
|
55
|
+
const composer = useComposerTrigger({ value, setValue: onChange, textareaRef, disabled, rootPath, enabled: enablePicker });
|
|
48
56
|
|
|
49
57
|
useOnClickOutside(wrapperRef, composer.close);
|
|
50
58
|
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
import type { TargetedMouseEvent } from 'preact';
|
|
17
17
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
18
18
|
import { renderMarkdown } from '@/shared/lib/markdown/marked';
|
|
19
|
+
import { rewriteDocumentReferences, type DocumentScope } from '@/shared/lib/markdown/document-urls';
|
|
19
20
|
import { hydrateMathBlocks, MATH_PENDING_CLASS, preloadKatex } from '@/shared/lib/markdown/katex';
|
|
20
21
|
import { sanitizeHtml } from '@/shared/lib/markdown/sanitize';
|
|
21
22
|
import { hydrateMermaidBlocks } from '@/shared/lib/markdown/mermaid';
|
|
22
23
|
import { copyToClipboard } from '@/client/hooks/ui/clipboard';
|
|
24
|
+
import { openFileInEditor } from '@/client/hooks/ui/open-file';
|
|
23
25
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
24
26
|
import { DiagramViewer } from '@/client/components/common/diagram-viewer';
|
|
25
27
|
import { readDiagramBlock, type DiagramSnapshot } from '@/client/components/common/diagram-viewer/read-block';
|
|
@@ -30,9 +32,17 @@ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : us
|
|
|
30
32
|
interface MarkdownRendererProps {
|
|
31
33
|
content: string;
|
|
32
34
|
className?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Render the content as the document it IS (an opened file's preview) rather
|
|
37
|
+
* than as agent output: the source's own HTML renders as markup instead of
|
|
38
|
+
* showing its tags, and relative references resolve against `scope`.
|
|
39
|
+
*/
|
|
40
|
+
document?: boolean;
|
|
41
|
+
/** Where the document lives, so its relative links and images resolve. */
|
|
42
|
+
scope?: DocumentScope;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
export function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
|
45
|
+
export function MarkdownRenderer({ content, className = '', document: isDocument = false, scope }: MarkdownRendererProps) {
|
|
36
46
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
37
47
|
/** Block the open viewer was read from, so a theme re-render can refresh it. */
|
|
38
48
|
const diagramBlockRef = useRef<HTMLElement | null>(null);
|
|
@@ -40,15 +50,24 @@ export function MarkdownRenderer({ content, className = '' }: MarkdownRendererPr
|
|
|
40
50
|
|
|
41
51
|
const syntaxReady = useSyntaxReady();
|
|
42
52
|
|
|
53
|
+
// Primitive deps: call sites build `scope` inline, and a new object identity
|
|
54
|
+
// per render would re-run marked + DOMPurify over the whole document.
|
|
55
|
+
const scopePath = scope?.path;
|
|
56
|
+
const scopeRoot = scope?.root;
|
|
57
|
+
const scopeRepo = scope?.repo;
|
|
58
|
+
|
|
43
59
|
const html = useMemo(() => {
|
|
44
60
|
if (!content) return '';
|
|
45
|
-
const rendered = renderMarkdown(content);
|
|
61
|
+
const rendered = renderMarkdown(content, { allowHtml: isDocument });
|
|
62
|
+
const scoped = isDocument && scopePath
|
|
63
|
+
? rewriteDocumentReferences(rendered, { path: scopePath, root: scopeRoot, repo: scopeRepo })
|
|
64
|
+
: rendered;
|
|
46
65
|
// If the message contains math, start fetching KaTeX during parse rather
|
|
47
66
|
// than waiting for the post-mount hydration effect — the request overlaps
|
|
48
67
|
// sanitization and the rest of the render instead of following it.
|
|
49
|
-
if (
|
|
50
|
-
return sanitizeHtml(
|
|
51
|
-
}, [content, syntaxReady]);
|
|
68
|
+
if (scoped.includes(MATH_PENDING_CLASS)) preloadKatex();
|
|
69
|
+
return sanitizeHtml(scoped);
|
|
70
|
+
}, [content, syntaxReady, isDocument, scopePath, scopeRoot, scopeRepo]);
|
|
52
71
|
|
|
53
72
|
const hasMermaid = html.includes('mermaid-block');
|
|
54
73
|
const hasMath = html.includes(MATH_PENDING_CLASS);
|
|
@@ -154,15 +173,34 @@ export function MarkdownRenderer({ content, className = '' }: MarkdownRendererPr
|
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
// A rendered diagram is a door into the zoom/pan viewer; pending, failed,
|
|
157
|
-
// and still-streaming blocks stay inert.
|
|
176
|
+
// and still-streaming blocks stay inert. It is checked before the document
|
|
177
|
+
// links below so a diagram's own anchors never steal its click.
|
|
158
178
|
const block = (e.target as HTMLElement).closest<HTMLElement>('.mermaid-block[data-mermaid-state="done"]');
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
if (block) {
|
|
180
|
+
const snapshot = readDiagramBlock(block);
|
|
181
|
+
if (!snapshot) return;
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
diagramBlockRef.current = block;
|
|
184
|
+
setDiagram(snapshot);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!isDocument) return;
|
|
189
|
+
|
|
190
|
+
// A document's own links: a relative one is rewritten to the workspace
|
|
191
|
+
// file it names, so it belongs in an editor tab rather than in a top-level
|
|
192
|
+
// navigation off the console; an external one leaves the console.
|
|
193
|
+
const link = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[href]');
|
|
194
|
+
if (!link) return;
|
|
162
195
|
e.preventDefault();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
const filePath = link.dataset.filePath;
|
|
198
|
+
if (filePath) {
|
|
199
|
+
openFileInEditor({ path: filePath, root: scopeRoot, repo: scopeRepo });
|
|
200
|
+
} else if (/^https?:/i.test(link.href)) {
|
|
201
|
+
window.open(link.href, '_blank', 'noopener,noreferrer');
|
|
202
|
+
}
|
|
203
|
+
}, [isDocument, scopeRoot, scopeRepo]);
|
|
166
204
|
|
|
167
205
|
const closeDiagram = useCallback(() => {
|
|
168
206
|
diagramBlockRef.current = null;
|
|
@@ -78,9 +78,15 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
78
78
|
<span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
|
|
79
79
|
</div>
|
|
80
80
|
) : isMd && isPreview ? (
|
|
81
|
-
/* Markdown Preview Mode
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/* Markdown Preview Mode — styled by `.prose-content` (the chat
|
|
82
|
+
timeline's system), see the desktop editor for why there is no
|
|
83
|
+
`prose` wrapper. */
|
|
84
|
+
<div className="p-4 bg-paper text-ink min-h-full font-sans">
|
|
85
|
+
<MarkdownRenderer
|
|
86
|
+
content={content}
|
|
87
|
+
document
|
|
88
|
+
scope={{ path: file.path ?? file.name, root: file.root, repo: file.repo }}
|
|
89
|
+
/>
|
|
84
90
|
</div>
|
|
85
91
|
) : (
|
|
86
92
|
/* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
|
|
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'preact/hooks';
|
|
|
2
2
|
import { Check, ChevronLeft, X } from 'lucide-preact';
|
|
3
3
|
import type { SettingsCategoryId, SettingsState } from '@/shared/types';
|
|
4
4
|
import { mergeChamberSettings } from '@/shared/lib/settings/client';
|
|
5
|
+
import { diffSettings } from '@/shared/lib/settings/diff';
|
|
5
6
|
import { useChamberSettingsWriter } from '@/client/hooks/settings/use-chamber-setting';
|
|
6
7
|
import { applyDocumentTheme } from '@/client/hooks/ui/theme';
|
|
8
|
+
import { DEFAULT_THEME_ID } from '@/shared/lib/theme/catalog';
|
|
7
9
|
import { useChamberEvent } from '@/client/hooks/ui/window-event';
|
|
8
10
|
import { SETTINGS_CATEGORIES, SettingsSidebar } from '@/client/components/settings/Sidebar';
|
|
9
11
|
import { AppearanceSettings } from '@/client/components/settings/categories/AppearanceSettings';
|
|
@@ -35,7 +37,7 @@ const DEFAULT_SETTINGS: SettingsState = {
|
|
|
35
37
|
showUpdateNotifications: true,
|
|
36
38
|
agentControlTool: true,
|
|
37
39
|
ompChamberWebTool: true,
|
|
38
|
-
theme:
|
|
40
|
+
theme: DEFAULT_THEME_ID,
|
|
39
41
|
fontSize: 'standard',
|
|
40
42
|
editorFont: 'JetBrains Mono',
|
|
41
43
|
streamResponses: true,
|
|
@@ -104,7 +106,14 @@ export function SettingsModal({
|
|
|
104
106
|
if (typeof document !== 'undefined' && next.theme) {
|
|
105
107
|
applyDocumentTheme(next.theme);
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
// Only the keys this update actually touched are persisted. The modal
|
|
110
|
+
// holds a full `SettingsState` snapshot taken when it opened, so writing
|
|
111
|
+
// the whole object back would also republish every value it read at that
|
|
112
|
+
// moment — a tab left open across an external change (another tab, a
|
|
113
|
+
// `curl` to /api/settings) would silently revert it on the next unrelated
|
|
114
|
+
// toggle. `theme` in particular is written by the appearance picker and
|
|
115
|
+
// read by the SSR shell, so the stale copy is not just cosmetic.
|
|
116
|
+
writeChamberSettings(diffSettings(prev, next, updater));
|
|
108
117
|
return next;
|
|
109
118
|
});
|
|
110
119
|
setToastMessage('Setting was saved');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Palette } from 'lucide-preact';
|
|
2
2
|
import type { SettingsState } from '@/shared/types';
|
|
3
|
+
import { resolveThemeId } from '@/shared/lib/theme/catalog';
|
|
4
|
+
import { ThemeGrid } from '@/client/components/settings/categories/appearance-settings/ThemeGrid';
|
|
3
5
|
|
|
4
6
|
interface AppearanceSettingsProps {
|
|
5
7
|
settings: SettingsState;
|
|
@@ -7,11 +9,6 @@ interface AppearanceSettingsProps {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsProps) {
|
|
10
|
-
const themes = [
|
|
11
|
-
{ id: 'paper', name: 'E-Ink Paper', desc: 'Tema terang bawaan E-Ink Monochrome.', icon: Sun },
|
|
12
|
-
{ id: 'one-dark-pro-soft', name: 'One Dark Pro Soft', desc: 'Tema gelap ala One Dark Pro.', icon: Moon }
|
|
13
|
-
] as const;
|
|
14
|
-
|
|
15
12
|
return (
|
|
16
13
|
<div className="w-full space-y-8 text-xs text-ink">
|
|
17
14
|
<section className="space-y-4">
|
|
@@ -20,30 +17,10 @@ export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsPro
|
|
|
20
17
|
<h3 className="font-semibold text-sm uppercase tracking-wider text-ink/80">Theme Preferences</h3>
|
|
21
18
|
</div>
|
|
22
19
|
|
|
23
|
-
<
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
onClick={() => onUpdate({ theme: theme.id })}
|
|
28
|
-
className={`flex items-start space-x-3 p-3 rounded-lg border text-left transition-all ${
|
|
29
|
-
settings.theme === theme.id
|
|
30
|
-
? 'border-ink bg-ink/5 ring-1 ring-ink/20'
|
|
31
|
-
: 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
|
|
32
|
-
}`}
|
|
33
|
-
>
|
|
34
|
-
<div className={`p-1.5 rounded-full ${settings.theme === theme.id ? 'bg-ink text-paper' : 'bg-canvas text-ink/60'}`}>
|
|
35
|
-
<theme.icon size={16} />
|
|
36
|
-
</div>
|
|
37
|
-
<div className="flex-1">
|
|
38
|
-
<div className="font-semibold text-ink">{theme.name}</div>
|
|
39
|
-
<div className="text-ink/60 mt-0.5 leading-relaxed">{theme.desc}</div>
|
|
40
|
-
</div>
|
|
41
|
-
{settings.theme === theme.id && (
|
|
42
|
-
<Check size={16} className="text-ink mt-1" />
|
|
43
|
-
)}
|
|
44
|
-
</button>
|
|
45
|
-
))}
|
|
46
|
-
</div>
|
|
20
|
+
<ThemeGrid
|
|
21
|
+
selectedId={settings.theme}
|
|
22
|
+
onSelect={(theme) => onUpdate({ theme: resolveThemeId(theme) })}
|
|
23
|
+
/>
|
|
47
24
|
</section>
|
|
48
25
|
</div>
|
|
49
26
|
);
|
|
@@ -20,6 +20,7 @@ export function ProviderSettings({
|
|
|
20
20
|
onAddModalClose,
|
|
21
21
|
}: ProviderSettingsProps) {
|
|
22
22
|
const {
|
|
23
|
+
providers,
|
|
23
24
|
connectedProviders,
|
|
24
25
|
availablePresetProviders,
|
|
25
26
|
selectedProvider,
|
|
@@ -107,6 +108,7 @@ export function ProviderSettings({
|
|
|
107
108
|
}}
|
|
108
109
|
onAddProvider={handleAddProvider}
|
|
109
110
|
presets={availablePresetProviders}
|
|
111
|
+
existingSlugs={providers.map((provider) => provider.slug)}
|
|
110
112
|
/>
|
|
111
113
|
|
|
112
114
|
{selectedProvider && (
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One theme as a palette card: the name, a light/dark badge, and the eight
|
|
8
|
+
* colors the theme actually paints with — canvas, surface, ink, error, success,
|
|
9
|
+
* info, warning and accent.
|
|
10
|
+
*
|
|
11
|
+
* The chips are the palette's own hex values, not the live CSS variables, so
|
|
12
|
+
* the card shows what selecting it will do rather than what is currently on
|
|
13
|
+
* screen. Hovering a chip names its role; the two surface colors carry the
|
|
14
|
+
* card's frame, which is why they lead the strip.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Check } from 'lucide-preact';
|
|
18
|
+
import type { ThemePalette } from '@/shared/types/theme';
|
|
19
|
+
import { THEME_SWATCHES } from '@/shared/lib/theme/catalog';
|
|
20
|
+
|
|
21
|
+
interface ThemeCardProps {
|
|
22
|
+
theme: ThemePalette;
|
|
23
|
+
selected: boolean;
|
|
24
|
+
onSelect: (id: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ThemeCard({ theme, selected, onSelect }: ThemeCardProps) {
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => onSelect(theme.id)}
|
|
32
|
+
aria-pressed={selected}
|
|
33
|
+
className={`group flex w-full flex-col gap-2.5 rounded-lg border p-2.5 text-left transition-all cursor-pointer ${
|
|
34
|
+
selected
|
|
35
|
+
? 'border-ink bg-ink/5 ring-1 ring-ink/20'
|
|
36
|
+
: 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
|
|
37
|
+
}`}
|
|
38
|
+
>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<span className="truncate font-semibold text-ink" title={theme.name}>
|
|
41
|
+
{theme.name}
|
|
42
|
+
</span>
|
|
43
|
+
<span className="shrink-0 rounded-full border border-ink/15 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-ink/50">
|
|
44
|
+
{theme.variant}
|
|
45
|
+
</span>
|
|
46
|
+
{selected && <Check size={14} className="ml-auto shrink-0 text-ink" />}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="flex items-center gap-1">
|
|
50
|
+
{THEME_SWATCHES.map((swatch) => (
|
|
51
|
+
<span
|
|
52
|
+
key={swatch.key}
|
|
53
|
+
title={`${swatch.label} · ${theme[swatch.key]}`}
|
|
54
|
+
style={{ backgroundColor: theme[swatch.key] }}
|
|
55
|
+
className="h-5 flex-1 rounded-[3px] border border-ink/10"
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The theme catalog as a filterable grid of palette cards.
|
|
8
|
+
*
|
|
9
|
+
* Two axes of grouping, both from the catalog: the light/dark filter narrows
|
|
10
|
+
* what is shown, and the family grouping keeps a theme's light and dark entries
|
|
11
|
+
* adjacent (`Nord` and `Nord` rather than `Nord` and `Monokai`). The filter is
|
|
12
|
+
* local state — it changes nothing about the app, only what the grid lists.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo, useState } from 'preact/hooks';
|
|
16
|
+
import { Moon, Sun } from 'lucide-preact';
|
|
17
|
+
import type { ThemePalette, ThemeVariant } from '@/shared/types/theme';
|
|
18
|
+
import { THEMES } from '@/shared/lib/theme/catalog';
|
|
19
|
+
import { ThemeCard } from '@/client/components/settings/categories/appearance-settings/ThemeCard';
|
|
20
|
+
|
|
21
|
+
interface ThemeGridProps {
|
|
22
|
+
selectedId: string;
|
|
23
|
+
onSelect: (id: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type VariantFilter = 'all' | ThemeVariant;
|
|
27
|
+
|
|
28
|
+
const FILTERS: ReadonlyArray<{ id: VariantFilter; label: string }> = [
|
|
29
|
+
{ id: 'all', label: 'All' },
|
|
30
|
+
{ id: 'light', label: 'Light' },
|
|
31
|
+
{ id: 'dark', label: 'Dark' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Catalog order is already family-grouped (see `catalog.ts`), so filtering is a
|
|
36
|
+
* plain pass — a second grouping step here would only risk disagreeing with it.
|
|
37
|
+
*/
|
|
38
|
+
function filterThemes(filter: VariantFilter): readonly ThemePalette[] {
|
|
39
|
+
return filter === 'all' ? THEMES : THEMES.filter((theme) => theme.variant === filter);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ThemeGrid({ selectedId, onSelect }: ThemeGridProps) {
|
|
43
|
+
const [filter, setFilter] = useState<VariantFilter>('all');
|
|
44
|
+
const themes = useMemo(() => filterThemes(filter), [filter]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-3 pb-6">
|
|
48
|
+
<div className="flex items-center gap-1.5">
|
|
49
|
+
{FILTERS.map((option) => (
|
|
50
|
+
<button
|
|
51
|
+
key={option.id}
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => setFilter(option.id)}
|
|
54
|
+
className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors cursor-pointer ${
|
|
55
|
+
filter === option.id
|
|
56
|
+
? 'border-ink bg-ink text-paper'
|
|
57
|
+
: 'border-ink/20 bg-paper text-ink/70 hover:border-ink/40 hover:text-ink'
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
{option.id === 'light' && <Sun size={11} className="mr-1 inline align-[-1px]" />}
|
|
61
|
+
{option.id === 'dark' && <Moon size={11} className="mr-1 inline align-[-1px]" />}
|
|
62
|
+
{option.label}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
<span className="ml-auto font-mono text-[11px] text-ink/50">{themes.length} themes</span>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
|
|
69
|
+
{themes.map((theme) => (
|
|
70
|
+
<ThemeCard
|
|
71
|
+
key={theme.id}
|
|
72
|
+
theme={theme}
|
|
73
|
+
selected={theme.id === selectedId}
|
|
74
|
+
onSelect={onSelect}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -20,7 +20,7 @@ const OPTIONS: Array<{
|
|
|
20
20
|
badge: 'Default',
|
|
21
21
|
icon: Sparkles,
|
|
22
22
|
description:
|
|
23
|
-
'
|
|
23
|
+
'As soon as your first message lands, the session is named from it using omp\'s own tiny title model — retried once when the run ends if that first attempt came back empty. Sessions stop showing "New Session - <timestamp>" in the sidebar.',
|
|
24
24
|
note: 'One small model call per session',
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -11,13 +11,24 @@ interface AddProviderModalProps {
|
|
|
11
11
|
onClose: () => void;
|
|
12
12
|
onAddProvider: (newProvider: ProviderItem, options: { fetchedCount: number }) => void;
|
|
13
13
|
presets: PresetProviderOption[];
|
|
14
|
+
/**
|
|
15
|
+
* Slugs already present in the omp registry. A new provider's slug becomes a
|
|
16
|
+
* `models.yml` key, so reusing one silently overwrites that provider's
|
|
17
|
+
* `baseUrl`, `apiKey`, `api` and per-model metadata — the entry is written
|
|
18
|
+
* under the same key, not merged into it.
|
|
19
|
+
*/
|
|
20
|
+
existingSlugs: string[];
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
/** Slug shape omp accepts as a `models.yml` provider key. */
|
|
24
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
25
|
+
|
|
16
26
|
export function AddProviderModal({
|
|
17
27
|
isOpen,
|
|
18
28
|
onClose,
|
|
19
29
|
onAddProvider,
|
|
20
30
|
presets,
|
|
31
|
+
existingSlugs,
|
|
21
32
|
}: AddProviderModalProps) {
|
|
22
33
|
const presetList = presets;
|
|
23
34
|
const initialPreset = presetList[0];
|
|
@@ -42,19 +53,35 @@ export function AddProviderModal({
|
|
|
42
53
|
|
|
43
54
|
if (!isOpen) return null;
|
|
44
55
|
|
|
56
|
+
const selectedPreset = presetList.find((p) => p.id === selectedPresetId);
|
|
57
|
+
const trimmedName = name.trim();
|
|
58
|
+
// Prefer the preset's own slug so a provider lands under the id omp knows it
|
|
59
|
+
// by; only a renamed provider gets one derived from its display name.
|
|
60
|
+
const slug = (trimmedName && selectedPreset && trimmedName === selectedPreset.name
|
|
61
|
+
? selectedPreset.slug
|
|
62
|
+
: trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
63
|
+
).toLowerCase();
|
|
64
|
+
const takenSlugs = new Set(existingSlugs.map((value) => value.trim().toLowerCase()));
|
|
65
|
+
const slugError = !slug
|
|
66
|
+
? 'Enter a provider name to derive an id.'
|
|
67
|
+
: !SLUG_PATTERN.test(slug)
|
|
68
|
+
? 'The derived id must start with a letter or digit.'
|
|
69
|
+
: takenSlugs.has(slug)
|
|
70
|
+
? `"${slug}" is already registered in models.yml — connecting it would overwrite that provider. Pick another name.`
|
|
71
|
+
: '';
|
|
72
|
+
|
|
45
73
|
const handleSubmit = async (e: { preventDefault: () => void }) => {
|
|
46
74
|
e.preventDefault();
|
|
47
|
-
if (!
|
|
75
|
+
if (!trimmedName || presetList.length === 0 || isSubmitting || slugError) return;
|
|
48
76
|
setIsSubmitting(true);
|
|
49
77
|
|
|
50
78
|
try {
|
|
51
|
-
const preset = presetList.find((p) => p.id === selectedPresetId);
|
|
52
79
|
const timestamp = Date.now();
|
|
53
80
|
const newProvider: ProviderItem = {
|
|
54
81
|
id: `provider-${timestamp}`,
|
|
55
|
-
name:
|
|
56
|
-
slug
|
|
57
|
-
icon:
|
|
82
|
+
name: trimmedName,
|
|
83
|
+
slug,
|
|
84
|
+
icon: selectedPreset?.icon || 'custom',
|
|
58
85
|
status: 'connected',
|
|
59
86
|
configuredIn: 'auth credentials',
|
|
60
87
|
baseUrl: baseUrl.trim(),
|
|
@@ -97,7 +124,7 @@ export function AddProviderModal({
|
|
|
97
124
|
</button>
|
|
98
125
|
<button
|
|
99
126
|
type="submit"
|
|
100
|
-
disabled={isSubmitting || presetList.length === 0}
|
|
127
|
+
disabled={isSubmitting || presetList.length === 0 || Boolean(slugError)}
|
|
101
128
|
className="px-3.5 py-1.5 rounded-md bg-ink text-canvas text-xs font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
|
|
102
129
|
>
|
|
103
130
|
{isSubmitting ? (
|
|
@@ -160,6 +187,16 @@ export function AddProviderModal({
|
|
|
160
187
|
placeholder="e.g. My Provider"
|
|
161
188
|
className="w-full bg-paper border border-ink/20 rounded-md px-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60"
|
|
162
189
|
/>
|
|
190
|
+
{slug && (
|
|
191
|
+
<p className="text-[10px] text-ink/40 mt-1 font-mono">
|
|
192
|
+
models.yml id: {slug}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
{slugError && (
|
|
196
|
+
<p className="text-[11px] text-error mt-1">
|
|
197
|
+
{slugError}
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
163
200
|
</div>
|
|
164
201
|
|
|
165
202
|
{/* Base URL */}
|