ompchamber 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.b938f4a693.css +2 -0
- package/dist/client/static/js/77.8022a800b5.js +139 -0
- package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
- package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
- package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
- package/dist/client/static/js/async/66.86544ad741.js +93 -0
- package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
- package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
- package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
- package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
- package/dist/client/static/js/async/e.6443e739a2.js +79 -0
- package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
- package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
- package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
- package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
- package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
- package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
- package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
- package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
- package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
- package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
- package/dist/client/static/js/async/{5b.a5ccbc19dc.js → q.b453193538.js} +10 -10
- package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
- package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
- package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
- package/dist/client/static/js/async/vt.20406a5604.js +1 -0
- package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
- package/dist/client/static/js/async/yx.af2936c368.js +10 -0
- package/dist/client/static/js/index.57b39ffa7f.js +34 -0
- package/package.json +1 -1
- package/src/client/components/common/file-icon/index.tsx +4 -1
- package/src/client/components/common/image-viewer/index.tsx +224 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
- package/src/client/components/settings/Modal.tsx +1 -0
- package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
- package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
- package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
- package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
- package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
- package/src/client/components/workspace/btw-panel/index.tsx +83 -0
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +37 -21
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +44 -40
- package/src/client/components/workspace/chat-timeline/index.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
- package/src/client/components/workspace/editor/index.tsx +45 -36
- package/src/client/hooks/chat/btw/index.ts +180 -0
- package/src/client/hooks/chat/btw/intercept.ts +43 -0
- package/src/client/hooks/chat/btw/mode.ts +179 -0
- package/src/client/hooks/chat/composer/attachments.ts +198 -0
- package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
- package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
- package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
- package/src/client/hooks/chat/composer/drop-references.ts +106 -0
- package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
- package/src/client/hooks/chat/composer/file-drop.ts +226 -0
- package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
- package/src/client/hooks/chat/composer/file-reads.ts +238 -0
- package/src/client/hooks/chat/composer/index.ts +7 -1
- package/src/client/hooks/chat/composer/pipeline.ts +181 -0
- package/src/client/hooks/chat/omp/index.ts +3 -3
- package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
- package/src/client/hooks/chat/timeline/actions.ts +16 -1
- package/src/client/hooks/chat/timeline/index.ts +16 -8
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +29 -65
- package/src/client/hooks/chat/timeline/session-load.ts +2 -1
- package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
- package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
- package/src/client/hooks/editor/file-actions.ts +49 -0
- package/src/client/hooks/editor/use-file-editor.ts +38 -27
- package/src/client/hooks/editor/use-file-transfer.ts +78 -0
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.ts +21 -287
- package/src/server/lib/btw/frames.test.ts +68 -0
- package/src/server/lib/btw/frames.ts +56 -0
- package/src/server/lib/btw/prompt.ts +28 -0
- package/src/server/lib/btw/registry.server.ts +141 -0
- package/src/server/lib/btw/runtime.server.ts +324 -0
- package/src/server/lib/btw/schema.server.ts +44 -0
- package/src/server/lib/btw/service.server.ts +171 -0
- package/src/server/lib/btw/session-copy.server.ts +213 -0
- package/src/server/lib/btw/session-copy.test.ts +166 -0
- package/src/server/lib/btw/store.server.ts +240 -0
- package/src/server/lib/db/schema.ts +110 -0
- package/src/server/lib/db/seed.ts +160 -0
- package/src/server/lib/db/workspace-sync.ts +70 -0
- package/src/server/lib/fs/root.ts +37 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
- package/src/server/lib/omp/rpc/manager.ts +27 -123
- package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
- package/src/server/lib/omp/session/auto-title.server.ts +152 -0
- package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
- package/src/server/lib/omp/session/blobs.server.ts +76 -0
- package/src/server/lib/omp/session/file-scan.ts +174 -0
- package/src/server/lib/omp/session/files.test.ts +147 -0
- package/src/server/lib/omp/session/files.ts +52 -35
- package/src/server/lib/omp/session/locator.ts +2 -2
- package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
- package/src/server/lib/omp/session/merge-stored.ts +171 -0
- package/src/server/lib/omp/session/messages.ts +34 -0
- package/src/server/lib/omp/session/reader.ts +2 -2
- package/src/server/lib/omp/session/subagent-presence.ts +9 -6
- package/src/server/lib/queue/delivery.server.test.ts +57 -0
- package/src/server/lib/queue/delivery.server.ts +25 -3
- package/src/server/plugins/ssr.ts +29 -2
- package/src/server/routes/btw/events.ts +70 -0
- package/src/server/routes/btw/index.ts +8 -0
- package/src/server/routes/btw/topics.ts +87 -0
- package/src/server/routes/btw/ws.ts +136 -0
- package/src/server/routes/chat/index.ts +2 -0
- package/src/server/routes/chat/session.ts +1 -115
- package/src/server/routes/chat/turns.ts +87 -0
- package/src/server/routes/fs/index.ts +4 -1
- package/src/server/routes/fs/raw.ts +85 -0
- package/src/server/routes/fs/read.ts +58 -3
- package/src/server/routes/index.ts +7 -1
- package/src/shared/lib/chat/attachments.test.ts +208 -0
- package/src/shared/lib/chat/attachments.ts +200 -15
- package/src/shared/lib/chat/btw/client.ts +94 -0
- package/src/shared/lib/chat/composer/client.test.ts +48 -0
- package/src/shared/lib/chat/composer/client.ts +6 -1
- package/src/shared/lib/chat/composer/composer.test.ts +39 -1
- package/src/shared/lib/chat/composer/trigger.ts +45 -1
- package/src/shared/lib/chat/omp/agent-events.ts +33 -80
- package/src/shared/lib/chat/omp/socket.ts +11 -6
- package/src/shared/lib/chat/omp/sse.ts +11 -6
- package/src/shared/lib/chat/omp/tool-results.ts +102 -0
- package/src/shared/lib/chat/omp/transport.ts +19 -3
- package/src/shared/lib/chat/text-file.ts +148 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
- package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
- package/src/shared/lib/chat/timeline/turns.ts +7 -0
- package/src/shared/lib/fs/file-kind.ts +50 -0
- package/src/shared/lib/fs/paths.ts +19 -1
- package/src/shared/lib/markdown/math.test.ts +20 -3
- package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
- package/src/shared/lib/omp/session/mapper.ts +24 -4
- package/src/shared/lib/omp/session/messages-map.ts +16 -3
- package/src/shared/lib/omp/session/messages-parse.ts +2 -2
- package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +39 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.b859cc1cc5.css +0 -2
- package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
- package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
- package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
- package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
- package/dist/client/static/js/async/q.23af2a358f.js +0 -21
- package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
- package/dist/client/static/js/async/s.222ee974f6.js +0 -2
- package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/index.6e065017eb.js +0 -33
- package/dist/client/static/js/jp.50817896f3.js +0 -139
- /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{5b.a5ccbc19dc.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.6e065017eb.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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>",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
FolderOpen,
|
|
10
10
|
} from 'lucide-preact';
|
|
11
11
|
import { BRAND_ICONS, BRAND_VIEWBOX, type BrandIconName } from '@/client/components/common/file-icon/brand-paths';
|
|
12
|
+
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
12
13
|
|
|
13
14
|
interface FileIconProps {
|
|
14
15
|
name: string;
|
|
@@ -110,7 +111,9 @@ export function FileIcon({ name, isFolder, isOpen, size = 14, className = '' }:
|
|
|
110
111
|
if (lowerName.endsWith('.md') || lowerName.endsWith('.txt')) {
|
|
111
112
|
return <FileText size={size} className={`text-ink/70 ${className}`} />;
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
// SVG is text the editor edits as code, but it is still a picture to the eye,
|
|
115
|
+
// so the icon stays the image glyph for it.
|
|
116
|
+
if (getImageMimeType(lowerName) || lowerName.endsWith('.svg')) {
|
|
114
117
|
return <FileImage size={size} color="#c084fc" className={className} />;
|
|
115
118
|
}
|
|
116
119
|
if (lowerName.endsWith('.sh') || lowerName.endsWith('.bash')) {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The picture surface used wherever a file is opened and turns out to be an
|
|
8
|
+
* image — the desktop editor panel, the phone's full-screen editor, and the
|
|
9
|
+
* chat `read` tool card. An image has no text, so this replaces the code
|
|
10
|
+
* surface entirely: an `<img>` against the raw-byte route, a checker backdrop
|
|
11
|
+
* for transparency, and zoom/pan that mirror the diagram viewer so the two
|
|
12
|
+
* full-size viewers behave the same way.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
16
|
+
import type { TargetedPointerEvent, TargetedWheelEvent } from 'preact';
|
|
17
|
+
import { AlertTriangle, Loader2 } from 'lucide-preact';
|
|
18
|
+
|
|
19
|
+
const MIN_SCALE = 0.1;
|
|
20
|
+
const MAX_SCALE = 16;
|
|
21
|
+
/** Zoom step for the buttons and the +/- keys. */
|
|
22
|
+
const ZOOM_STEP = 1.4;
|
|
23
|
+
/** Wheel `deltaY` → scale exponent, matching the diagram viewer's feel. */
|
|
24
|
+
const WHEEL_SENSITIVITY = 0.0015;
|
|
25
|
+
/** Pixels kept clear around a fitted picture. */
|
|
26
|
+
const FIT_MARGIN = 48;
|
|
27
|
+
|
|
28
|
+
interface ImageViewerProps {
|
|
29
|
+
/** Raw-byte URL; a different URL is a different picture. */
|
|
30
|
+
src: string;
|
|
31
|
+
name: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Drag {
|
|
36
|
+
pointerId: number;
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The stateful surface, keyed on `src` by `ImageViewer` — remounting on a new
|
|
43
|
+
* URL is what guarantees a refresh cannot inherit the previous picture's fit,
|
|
44
|
+
* pan offset, or decode error.
|
|
45
|
+
*/
|
|
46
|
+
function ImageSurface({ src, name, className = '' }: ImageViewerProps) {
|
|
47
|
+
const stageRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
49
|
+
const dragRef = useRef<Drag | null>(null);
|
|
50
|
+
|
|
51
|
+
const [natural, setNatural] = useState<{ width: number; height: number } | null>(null);
|
|
52
|
+
const [scale, setScale] = useState(1);
|
|
53
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
54
|
+
const [dragging, setDragging] = useState(false);
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
/** Read the decoded size off the element; false while it is not decodable yet. */
|
|
58
|
+
const measure = useCallback((): boolean => {
|
|
59
|
+
const image = imageRef.current;
|
|
60
|
+
if (!image || !image.complete || image.naturalWidth === 0) return false;
|
|
61
|
+
setNatural({ width: image.naturalWidth, height: image.naturalHeight });
|
|
62
|
+
return true;
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// A cached picture can be complete before its `load` handler is attached, so
|
|
66
|
+
// the size is read once on mount as well — otherwise a re-opened image sat
|
|
67
|
+
// at 100% with no dimensions reported and Fit had nothing to fit to.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
measure();
|
|
70
|
+
}, [measure]);
|
|
71
|
+
|
|
72
|
+
/** Scale that fits the whole picture without upscaling it past 1. */
|
|
73
|
+
const fitToView = useCallback(() => {
|
|
74
|
+
const image = imageRef.current;
|
|
75
|
+
const stage = stageRef.current?.getBoundingClientRect();
|
|
76
|
+
const width = image?.naturalWidth ?? 0;
|
|
77
|
+
const height = image?.naturalHeight ?? 0;
|
|
78
|
+
if (!stage || !width || !height) return;
|
|
79
|
+
setScale(
|
|
80
|
+
Math.min(
|
|
81
|
+
1,
|
|
82
|
+
Math.max(MIN_SCALE, (stage.width - FIT_MARGIN) / width, (stage.height - FIT_MARGIN) / height),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
setOffset({ x: 0, y: 0 });
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Fit once per picture. Re-measuring on every render would fight an
|
|
89
|
+
// in-progress zoom, and `natural` only changes for a new decode.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (natural) fitToView();
|
|
92
|
+
}, [natural, fitToView]);
|
|
93
|
+
|
|
94
|
+
const zoomBy = useCallback((factor: number) => {
|
|
95
|
+
setScale((current) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, current * factor)));
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const handleWheel = useCallback(
|
|
99
|
+
(event: TargetedWheelEvent<HTMLDivElement>) => {
|
|
100
|
+
// Without this the editor panel scrolls while zooming.
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
zoomBy(Math.exp(-event.deltaY * WHEEL_SENSITIVITY));
|
|
103
|
+
},
|
|
104
|
+
[zoomBy],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const handlePointerDown = useCallback((event: TargetedPointerEvent<HTMLDivElement>) => {
|
|
108
|
+
if (event.button !== 0 || !natural) return;
|
|
109
|
+
dragRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
|
|
110
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
111
|
+
setDragging(true);
|
|
112
|
+
}, [natural]);
|
|
113
|
+
|
|
114
|
+
const handlePointerMove = useCallback((event: TargetedPointerEvent<HTMLDivElement>) => {
|
|
115
|
+
const drag = dragRef.current;
|
|
116
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
117
|
+
const dx = event.clientX - drag.x;
|
|
118
|
+
const dy = event.clientY - drag.y;
|
|
119
|
+
drag.x = event.clientX;
|
|
120
|
+
drag.y = event.clientY;
|
|
121
|
+
setOffset((current) => ({ x: current.x + dx, y: current.y + dy }));
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const endDrag = useCallback((event: TargetedPointerEvent<HTMLDivElement>) => {
|
|
125
|
+
if (dragRef.current?.pointerId !== event.pointerId) return;
|
|
126
|
+
dragRef.current = null;
|
|
127
|
+
setDragging(false);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const handleDoubleClick = useCallback(() => {
|
|
131
|
+
if (Math.abs(scale - 1) < 0.01) fitToView();
|
|
132
|
+
else {
|
|
133
|
+
setScale(1);
|
|
134
|
+
setOffset({ x: 0, y: 0 });
|
|
135
|
+
}
|
|
136
|
+
}, [scale, fitToView]);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
140
|
+
if (event.key !== '+' && event.key !== '=' && event.key !== '-' && event.key !== '_' && event.key !== '0') return;
|
|
141
|
+
// The stage owns these keys only while it has focus; a window-level
|
|
142
|
+
// listener would steal `-`/`0` from the composer and the file search box.
|
|
143
|
+
const target = event.target as HTMLElement | null;
|
|
144
|
+
if (!target || !stageRef.current?.contains(target)) return;
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
if (event.key === '0') fitToView();
|
|
147
|
+
else if (event.key === '+' || event.key === '=') zoomBy(ZOOM_STEP);
|
|
148
|
+
else zoomBy(1 / ZOOM_STEP);
|
|
149
|
+
};
|
|
150
|
+
window.addEventListener('keydown', onKeyDown);
|
|
151
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
152
|
+
}, [zoomBy, fitToView]);
|
|
153
|
+
|
|
154
|
+
const percent = Math.round(scale * 100);
|
|
155
|
+
const dimensions = natural ? `${natural.width} × ${natural.height} px` : '';
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className={`relative flex h-full w-full flex-col bg-paper ${className}`}>
|
|
159
|
+
<div
|
|
160
|
+
ref={stageRef}
|
|
161
|
+
tabIndex={-1}
|
|
162
|
+
onWheel={handleWheel}
|
|
163
|
+
onPointerDown={handlePointerDown}
|
|
164
|
+
onPointerMove={handlePointerMove}
|
|
165
|
+
onPointerUp={endDrag}
|
|
166
|
+
onPointerCancel={endDrag}
|
|
167
|
+
onDblClick={handleDoubleClick}
|
|
168
|
+
className={`image-stage relative flex-1 overflow-hidden outline-none ${dragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
|
169
|
+
>
|
|
170
|
+
{error ? (
|
|
171
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 px-6 text-center">
|
|
172
|
+
<AlertTriangle size={20} className="text-error" />
|
|
173
|
+
<span className="font-sans text-xs text-error">Could not read {name}</span>
|
|
174
|
+
<span className="break-all font-mono text-[11px] text-ink/50">{error}</span>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
{!natural && (
|
|
179
|
+
<div className="absolute inset-0 flex items-center justify-center gap-2 text-xs text-ink/40">
|
|
180
|
+
<Loader2 size={14} className="animate-spin" />
|
|
181
|
+
<span>Loading image…</span>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
185
|
+
<img
|
|
186
|
+
ref={imageRef}
|
|
187
|
+
src={src}
|
|
188
|
+
alt={name}
|
|
189
|
+
draggable={false}
|
|
190
|
+
onLoad={measure}
|
|
191
|
+
onError={() => setError('The file could not be decoded as an image.')}
|
|
192
|
+
className="image-canvas max-h-none max-w-none select-none"
|
|
193
|
+
style={{
|
|
194
|
+
transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${scale})`,
|
|
195
|
+
visibility: natural ? 'visible' : 'hidden',
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="flex flex-shrink-0 items-center justify-between border-t border-ink/10 bg-canvas px-3 py-1.5 font-mono text-[10px] text-ink/50">
|
|
204
|
+
<span>{dimensions}</span>
|
|
205
|
+
<div className="flex items-center gap-3">
|
|
206
|
+
<button type="button" onClick={() => zoomBy(1 / ZOOM_STEP)} className="hover:text-ink" title="Zoom out (-)">
|
|
207
|
+
−
|
|
208
|
+
</button>
|
|
209
|
+
<span className="w-10 text-center tabular-nums">{percent}%</span>
|
|
210
|
+
<button type="button" onClick={() => zoomBy(ZOOM_STEP)} className="hover:text-ink" title="Zoom in (+)">
|
|
211
|
+
+
|
|
212
|
+
</button>
|
|
213
|
+
<button type="button" onClick={fitToView} className="hover:text-ink" title="Fit to view (0)">
|
|
214
|
+
Fit
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function ImageViewer(props: ImageViewerProps) {
|
|
223
|
+
return <ImageSurface key={props.src} {...props} />;
|
|
224
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useState } from 'preact/hooks';
|
|
|
2
2
|
import { AlertTriangle, Loader2 } from 'lucide-preact';
|
|
3
3
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
4
4
|
import { CodeSurface } from '@/client/components/common/code-surface';
|
|
5
|
+
import { ImageViewer } from '@/client/components/common/image-viewer';
|
|
5
6
|
import { EditorHeader } from '@/client/components/mobile/mobile-right-sidebar/Header';
|
|
6
7
|
import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
|
|
7
8
|
import { useFileEditor } from '@/client/hooks/editor/use-file-editor';
|
|
@@ -48,6 +49,7 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
48
49
|
isPreview={isPreview}
|
|
49
50
|
wordWrap={wordWrap}
|
|
50
51
|
copied={editor.copied}
|
|
52
|
+
isImage={editor.isImage}
|
|
51
53
|
onClose={onClose}
|
|
52
54
|
onTogglePreview={() => setIsPreview(!isPreview)}
|
|
53
55
|
onToggleWrap={() => setWordWrap(!wordWrap)}
|
|
@@ -57,46 +59,52 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
57
59
|
onDownload={editor.download}
|
|
58
60
|
/>
|
|
59
61
|
|
|
60
|
-
{/* Editor Content Area - Desktop styled: bg-paper text-ink
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
62
|
+
{/* Editor Content Area - Desktop styled: bg-paper text-ink. An image owns
|
|
63
|
+
its own pan/zoom, so it replaces the scrolling text container rather
|
|
64
|
+
than living inside it. */}
|
|
65
|
+
{editor.isImage && editor.imageUrl ? (
|
|
66
|
+
<ImageViewer src={editor.imageUrl} name={file.name} className="flex-1 min-h-0" />
|
|
67
|
+
) : (
|
|
68
|
+
<div onScroll={handleScroll} className={`flex-1 min-h-0 overflow-auto relative bg-paper text-ink select-text ${scrollbarFadeClass(isScrolling)}`}>
|
|
69
|
+
{editor.isLoading ? (
|
|
70
|
+
<div className="flex items-center justify-center h-full text-ink/40 text-xs gap-2">
|
|
71
|
+
<Loader2 size={14} className="animate-spin" />
|
|
72
|
+
<span>Loading file content...</span>
|
|
73
|
+
</div>
|
|
74
|
+
) : editor.loadError ? (
|
|
75
|
+
<div className="flex flex-col items-center justify-center h-full gap-2 px-6 text-center">
|
|
76
|
+
<AlertTriangle size={20} className="text-error" />
|
|
77
|
+
<span className="text-xs text-error font-sans">Could not read {file.name}</span>
|
|
78
|
+
<span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
|
|
79
|
+
</div>
|
|
80
|
+
) : isMd && isPreview ? (
|
|
81
|
+
/* Markdown Preview Mode */
|
|
82
|
+
<div className="p-4 bg-paper text-ink min-h-full font-sans prose prose-sm max-w-none">
|
|
83
|
+
<MarkdownRenderer content={content} />
|
|
84
|
+
</div>
|
|
85
|
+
) : (
|
|
86
|
+
/* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
|
|
87
|
+
<CodeSurface
|
|
88
|
+
value={content}
|
|
89
|
+
onValueChange={editor.onChange}
|
|
90
|
+
language={lang}
|
|
91
|
+
wordWrap={wordWrap}
|
|
92
|
+
rootClassName="flex min-h-full bg-paper"
|
|
93
|
+
gutterClassName="w-10 py-3 pr-2 select-none text-right text-[10px] text-ink/30 bg-canvas border-r border-ink/10 font-mono leading-[20px] flex-shrink-0"
|
|
94
|
+
editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
|
|
95
|
+
editorClassName="focus:outline-none"
|
|
96
|
+
editorStyle={{
|
|
97
|
+
fontFamily: 'monospace',
|
|
98
|
+
fontSize: `${fontSize}px`,
|
|
99
|
+
lineHeight: '20px',
|
|
100
|
+
backgroundColor: 'transparent',
|
|
101
|
+
minHeight: '100%',
|
|
102
|
+
color: 'var(--theme-ink)',
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
100
108
|
|
|
101
109
|
{/* Bottom Status Bar - Desktop styled: bg-canvas border-t border-ink/10 */}
|
|
102
110
|
<footer
|
|
@@ -109,9 +117,15 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
109
117
|
}}
|
|
110
118
|
>
|
|
111
119
|
<div className="flex items-center space-x-3">
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
{editor.isImage ? (
|
|
121
|
+
<span className="uppercase">Image</span>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<span>{linesCount} lines</span>
|
|
125
|
+
<span>{content.length} chars</span>
|
|
126
|
+
<span className="uppercase">{lang}</span>
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
115
129
|
</div>
|
|
116
130
|
<div className="flex items-center space-x-2">
|
|
117
131
|
{editor.saveStatus === 'saving' && (
|
|
@@ -8,6 +8,8 @@ interface EditorHeaderProps {
|
|
|
8
8
|
isPreview: boolean;
|
|
9
9
|
wordWrap: boolean;
|
|
10
10
|
copied: boolean;
|
|
11
|
+
/** Raster image: wrap and font zoom are text-only, so both are hidden. */
|
|
12
|
+
isImage?: boolean;
|
|
11
13
|
onClose: () => void;
|
|
12
14
|
onTogglePreview: () => void;
|
|
13
15
|
onToggleWrap: () => void;
|
|
@@ -24,6 +26,7 @@ export function EditorHeader({
|
|
|
24
26
|
isPreview,
|
|
25
27
|
wordWrap,
|
|
26
28
|
copied,
|
|
29
|
+
isImage = false,
|
|
27
30
|
onClose,
|
|
28
31
|
onTogglePreview,
|
|
29
32
|
onToggleWrap,
|
|
@@ -71,32 +74,36 @@ export function EditorHeader({
|
|
|
71
74
|
</button>
|
|
72
75
|
)}
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
{!isImage && (
|
|
78
|
+
<>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={onToggleWrap}
|
|
82
|
+
className={`p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer ${wordWrap ? 'text-ink' : ''}`}
|
|
83
|
+
title="Toggle word wrap"
|
|
84
|
+
>
|
|
85
|
+
<WrapText size={15} />
|
|
86
|
+
</button>
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={onZoomOut}
|
|
91
|
+
className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
|
|
92
|
+
title="Decrease font size"
|
|
93
|
+
>
|
|
94
|
+
<ZoomOut size={15} />
|
|
95
|
+
</button>
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={onZoomIn}
|
|
100
|
+
className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
|
|
101
|
+
title="Increase font size"
|
|
102
|
+
>
|
|
103
|
+
<ZoomIn size={15} />
|
|
104
|
+
</button>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
100
107
|
|
|
101
108
|
<button
|
|
102
109
|
type="button"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SettingsState } from '@/shared/types';
|
|
2
2
|
import { ChatFollowUpSection } from '@/client/components/settings/categories/chat-settings/FollowUpSection';
|
|
3
3
|
import { ChatKeybindingsSection } from '@/client/components/settings/categories/chat-settings/KeybindingsSection';
|
|
4
|
+
import { ChatTitleSection } from '@/client/components/settings/categories/chat-settings/TitleSection';
|
|
4
5
|
import { ChatTransportSection } from '@/client/components/settings/categories/chat-settings/TransportSection';
|
|
5
6
|
|
|
6
7
|
interface ChatSettingsProps {
|
|
@@ -14,10 +15,13 @@ export function ChatSettings({ settings, onUpdate }: ChatSettingsProps) {
|
|
|
14
15
|
{/* 1. Streaming Transport (WebSocket vs SSE) */}
|
|
15
16
|
<ChatTransportSection settings={settings} onUpdate={onUpdate} />
|
|
16
17
|
|
|
17
|
-
{/* 2.
|
|
18
|
+
{/* 2. Session Titles (automatic generation) */}
|
|
19
|
+
<ChatTitleSection settings={settings} onUpdate={onUpdate} />
|
|
20
|
+
|
|
21
|
+
{/* 3. Follow-up Behavior (Interactive Selector Cards) */}
|
|
18
22
|
<ChatFollowUpSection settings={settings} onUpdate={onUpdate} />
|
|
19
23
|
|
|
20
|
-
{/*
|
|
24
|
+
{/* 4. Keyboard Shortcuts Configurator */}
|
|
21
25
|
<ChatKeybindingsSection settings={settings} onUpdate={onUpdate} />
|
|
22
26
|
</div>
|
|
23
27
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Check, Sparkles, Type } from 'lucide-preact';
|
|
2
|
+
import type { SettingsState } from '@/shared/types';
|
|
3
|
+
|
|
4
|
+
interface ChatTitleSectionProps {
|
|
5
|
+
settings: SettingsState;
|
|
6
|
+
onUpdate: (updater: Partial<SettingsState> | ((prev: SettingsState) => SettingsState)) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const OPTIONS: Array<{
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
label: string;
|
|
12
|
+
badge: string;
|
|
13
|
+
icon: typeof Sparkles;
|
|
14
|
+
description: string;
|
|
15
|
+
note: string;
|
|
16
|
+
}> = [
|
|
17
|
+
{
|
|
18
|
+
enabled: true,
|
|
19
|
+
label: 'Generate Automatically',
|
|
20
|
+
badge: 'Default',
|
|
21
|
+
icon: Sparkles,
|
|
22
|
+
description:
|
|
23
|
+
'After the first run settles, the session is named from the conversation using omp\'s own tiny title model. Sessions stop showing "New Session - <timestamp>" in the sidebar.',
|
|
24
|
+
note: 'One small model call per session',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
enabled: false,
|
|
28
|
+
label: 'Manual Only',
|
|
29
|
+
badge: 'Opt-out',
|
|
30
|
+
icon: Type,
|
|
31
|
+
description:
|
|
32
|
+
'Sessions keep their timestamp placeholder until you rename them from the sidebar. Choose this when every title should be deliberate, or to avoid the extra model call.',
|
|
33
|
+
note: 'No background model calls',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function ChatTitleSection({ settings, onUpdate }: ChatTitleSectionProps) {
|
|
38
|
+
const active = settings.autoSessionTitle ?? true;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<section className="space-y-3">
|
|
42
|
+
<div>
|
|
43
|
+
<h4 className="text-sm font-semibold text-ink">Session Titles</h4>
|
|
44
|
+
<p className="text-[11px] text-ink/60 mt-0.5">
|
|
45
|
+
How a session gets its display name. A name you set yourself is never replaced.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
50
|
+
{OPTIONS.map(({ enabled, label, badge, icon: Icon, description, note }) => {
|
|
51
|
+
const selected = active === enabled;
|
|
52
|
+
const select = () => onUpdate({ autoSessionTitle: enabled });
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
key={label}
|
|
56
|
+
onClick={select}
|
|
57
|
+
role="button"
|
|
58
|
+
tabIndex={0}
|
|
59
|
+
onKeyDown={(e) => e.key === 'Enter' && select()}
|
|
60
|
+
className={`p-3.5 rounded-lg border text-left transition-all cursor-pointer flex flex-col justify-between select-none ${
|
|
61
|
+
selected
|
|
62
|
+
? 'border-ink bg-ink/5 ring-1 ring-ink/20 shadow-xs'
|
|
63
|
+
: 'border-ink/15 bg-paper hover:border-ink/30 hover:bg-ink/[0.02]'
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
<div>
|
|
67
|
+
<div className="flex items-center justify-between mb-2">
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
<div
|
|
70
|
+
className={`w-7 h-7 rounded-md flex items-center justify-center ${
|
|
71
|
+
selected ? 'bg-ink text-paper' : 'bg-ink/5 text-ink/70'
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
<Icon size={15} />
|
|
75
|
+
</div>
|
|
76
|
+
<span className="font-semibold text-xs text-ink">{label}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<span className="text-[10px] font-mono uppercase px-1.5 py-0.5 rounded bg-ink/10 text-ink/70 font-medium">
|
|
79
|
+
{badge}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<p className="text-[11px] text-ink/70 leading-relaxed">{description}</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="mt-3 pt-2.5 border-t border-ink/10 flex items-center justify-between text-[11px]">
|
|
86
|
+
<span className="text-ink/50">{note}</span>
|
|
87
|
+
{selected && (
|
|
88
|
+
<span className="flex items-center gap-1 text-ink font-medium">
|
|
89
|
+
<Check size={12} /> Active
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
);
|
|
99
|
+
}
|