ompchamber 1.0.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.0d6ae50fe8.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/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- 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/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- 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 +39 -20
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- 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 +67 -43
- package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- 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/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- 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 +35 -9
- 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 +40 -4
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +29 -9
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +45 -70
- 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/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- 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/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -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/rpc/utility.ts +7 -1
- 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 +260 -0
- package/src/server/lib/queue/delivery.server.ts +63 -11
- 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/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- 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 +23 -9
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +195 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +154 -21
- 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/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- 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/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +77 -66
- package/src/shared/types/index.ts +2 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.e880517fcd.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.0de15c5e17.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.0d6ae50fe8.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.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -33,6 +33,15 @@ export function SessionItem({
|
|
|
33
33
|
onToggleExpand,
|
|
34
34
|
}: SessionItemProps) {
|
|
35
35
|
const showChevron = Boolean(expandable && hasSubagents && onToggleExpand);
|
|
36
|
+
// The slot shows one glyph, never two side by side. A live run signal owns
|
|
37
|
+
// it at rest — the spinner, or the blocked-on-a-question cue, which says it
|
|
38
|
+
// is YOUR turn. With nothing live to report the chevron is PINNED, collapsed
|
|
39
|
+
// or expanded: it is the only affordance left worth showing, and making it
|
|
40
|
+
// hover-only would leave the roster reachable only by hovering exactly the
|
|
41
|
+
// right 16px.
|
|
42
|
+
const liveRun = awaitingInput || status === 'stream';
|
|
43
|
+
const chevronPinned = showChevron && !liveRun;
|
|
44
|
+
const showStatus = Boolean(awaitingInput || status) && !chevronPinned;
|
|
36
45
|
const {
|
|
37
46
|
isEditing,
|
|
38
47
|
draft,
|
|
@@ -52,11 +61,16 @@ export function SessionItem({
|
|
|
52
61
|
: 'text-ink/75 hover:text-ink hover:bg-ink/5'
|
|
53
62
|
}`}
|
|
54
63
|
>
|
|
55
|
-
{/* One 16px slot owns both icons
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
{/* One 16px slot owns both icons — never two side by side. A live run
|
|
65
|
+
signal (blocked question, run spinner) holds the slot; otherwise the
|
|
66
|
+
chevron is pinned, collapsed or expanded. While the signal holds it,
|
|
67
|
+
hovering hands the slot to the chevron so the roster stays reachable
|
|
68
|
+
mid-run.
|
|
69
|
+
Both layers are pointer-inert for the opposite state: the toggle
|
|
70
|
+
while hidden, the status glyph ALWAYS (it is painted after the
|
|
71
|
+
toggle, so without that it wins the hit test over the revealed
|
|
72
|
+
chevron and every click on a badged row selects the session instead
|
|
73
|
+
of expanding it). */}
|
|
60
74
|
<span className="relative w-4 h-4 shrink-0">
|
|
61
75
|
{showChevron && onToggleExpand && (
|
|
62
76
|
<button
|
|
@@ -67,18 +81,18 @@ export function SessionItem({
|
|
|
67
81
|
}}
|
|
68
82
|
title={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
|
|
69
83
|
aria-expanded={isExpanded}
|
|
70
|
-
className={`peer absolute inset-0 flex items-center justify-center rounded cursor-pointer
|
|
71
|
-
|
|
72
|
-
? ''
|
|
73
|
-
: 'pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:opacity-100'
|
|
84
|
+
className={`peer absolute inset-0 flex items-center justify-center rounded cursor-pointer transition-opacity ${
|
|
85
|
+
chevronPinned
|
|
86
|
+
? 'text-ink/60 hover:text-ink'
|
|
87
|
+
: 'text-ink/40 hover:text-ink pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:opacity-100'
|
|
74
88
|
}`}
|
|
75
89
|
>
|
|
76
|
-
{isExpanded ? <ChevronDown size={13}
|
|
90
|
+
{isExpanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
|
77
91
|
</button>
|
|
78
92
|
)}
|
|
79
|
-
{
|
|
93
|
+
{showStatus && (
|
|
80
94
|
<span
|
|
81
|
-
className={`absolute inset-0 flex items-center justify-center text-ink/60 transition-opacity ${
|
|
95
|
+
className={`pointer-events-none absolute inset-0 flex items-center justify-center text-ink/60 transition-opacity ${
|
|
82
96
|
showChevron ? 'group-hover/item:opacity-0 peer-focus-visible:opacity-0' : ''
|
|
83
97
|
}`}
|
|
84
98
|
>
|
|
@@ -3,6 +3,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
|
|
|
3
3
|
import { SubagentStatusIcon } from '@/client/components/common/SubagentStatusIcon';
|
|
4
4
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
5
5
|
import { fetchSubagentHistory, historyEntryToSubagentInfo } from '@/shared/lib/omp/subagent/history/client';
|
|
6
|
+
import { subagentRowLabel } from '@/shared/lib/omp/subagent/label';
|
|
6
7
|
import { mergeSubagentRoster, parseSubagentLifecycle, parseSubagentProgress, parseSubagentRosterResponse } from '@/shared/lib/omp/subagent/parse';
|
|
7
8
|
import type { SubagentInfo, SubagentProgress } from '@/shared/types';
|
|
8
9
|
|
|
@@ -123,13 +124,14 @@ export function SubagentList({ sessionId, isActiveSession }: SubagentListProps)
|
|
|
123
124
|
</div>
|
|
124
125
|
) : (
|
|
125
126
|
subagents.map((subagent) => {
|
|
126
|
-
const
|
|
127
|
+
const label = subagentRowLabel(subagent);
|
|
128
|
+
const brief = subagent.task ?? subagent.assignment ?? subagent.description ?? label;
|
|
127
129
|
const isViewed = subagent.id === viewedSubagentId;
|
|
128
130
|
return (
|
|
129
131
|
<button
|
|
130
132
|
key={subagent.id}
|
|
131
133
|
type="button"
|
|
132
|
-
title={
|
|
134
|
+
title={brief}
|
|
133
135
|
onClick={() => window.dispatchEvent(new CustomEvent('omp:view-subagent', { detail: { sessionId: String(sessionId), subagent } }))}
|
|
134
136
|
className={`w-full flex items-center text-left text-xs rounded-md px-2 py-1 cursor-pointer transition-colors select-none group/subagent ${isViewed ? 'bg-ink/10 font-medium text-ink' : 'text-ink/70 hover:text-ink hover:bg-ink/5'}`}
|
|
135
137
|
>
|
|
@@ -141,8 +143,8 @@ export function SubagentList({ sessionId, isActiveSession }: SubagentListProps)
|
|
|
141
143
|
{/* Gap between subagent icon and task text */}
|
|
142
144
|
<span className="w-2 shrink-0" />
|
|
143
145
|
|
|
144
|
-
{/* Subagent
|
|
145
|
-
<span className="flex-1 min-w-0 truncate leading-snug">{
|
|
146
|
+
{/* Subagent label: `id (agent): target` */}
|
|
147
|
+
<span className="flex-1 min-w-0 truncate leading-snug">{label}</span>
|
|
146
148
|
</button>
|
|
147
149
|
);
|
|
148
150
|
})
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The ask card — the top card of the side-question form.
|
|
8
|
+
*
|
|
9
|
+
* It is the form's own entry, not the chat's: the question typed here is always
|
|
10
|
+
* a question of its own (a new topic), which is why it carries its own `⟳`
|
|
11
|
+
* (start over) and `✕` (leave the form). The topic history and promotion ride
|
|
12
|
+
* along here because the form replaced the composer that used to hold them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
16
|
+
import type { TargetedKeyboardEvent } from 'preact';
|
|
17
|
+
import { ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-preact';
|
|
18
|
+
import type { BtwTopic } from '@/shared/types';
|
|
19
|
+
import { BtwTopicMenu } from '@/client/components/workspace/btw-panel/TopicMenu';
|
|
20
|
+
|
|
21
|
+
export interface BtwAskCardProps {
|
|
22
|
+
value: string;
|
|
23
|
+
onChange: (value: string) => void;
|
|
24
|
+
onSubmit: () => void;
|
|
25
|
+
/** `⟳` — clear the question and fall back to the newest topic. */
|
|
26
|
+
onNewQuestion: () => void;
|
|
27
|
+
/** `✕` — leave the side-question form. */
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
/** A side answer is streaming: starting over waits for it. */
|
|
30
|
+
running: boolean;
|
|
31
|
+
topics: BtwTopic[];
|
|
32
|
+
activeTopicId: string | null;
|
|
33
|
+
onSelectTopic: (id: string) => void;
|
|
34
|
+
onDeleteTopic: (id: string) => void;
|
|
35
|
+
canPromote: boolean;
|
|
36
|
+
onPromote: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GHOST_BUTTON =
|
|
40
|
+
'p-1.5 rounded text-ink/45 hover:text-ink hover:bg-ink/5 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-ink/45';
|
|
41
|
+
|
|
42
|
+
export function BtwAskCard({
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
onSubmit,
|
|
46
|
+
onNewQuestion,
|
|
47
|
+
onClose,
|
|
48
|
+
running,
|
|
49
|
+
topics,
|
|
50
|
+
activeTopicId,
|
|
51
|
+
onSelectTopic,
|
|
52
|
+
onDeleteTopic,
|
|
53
|
+
canPromote,
|
|
54
|
+
onPromote,
|
|
55
|
+
}: BtwAskCardProps) {
|
|
56
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
57
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
58
|
+
|
|
59
|
+
// Grow with the text and collapse again once it is submitted or cleared.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const el = textareaRef.current;
|
|
62
|
+
if (!el) return;
|
|
63
|
+
el.style.height = 'auto';
|
|
64
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
65
|
+
}, [value]);
|
|
66
|
+
|
|
67
|
+
const handleKeyDown = (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => {
|
|
68
|
+
if (event.key === 'Escape') {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
onClose();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (event.key !== 'Enter' || event.shiftKey) return;
|
|
74
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
onSubmit();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="bg-paper border border-ink/20 rounded-2xl shadow-sm flex items-center gap-1 pl-1.5 pr-2 py-1 focus-within:border-ink transition-colors">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => {
|
|
84
|
+
onNewQuestion();
|
|
85
|
+
// The button took the focus; asking again means typing again.
|
|
86
|
+
textareaRef.current?.focus();
|
|
87
|
+
}}
|
|
88
|
+
disabled={running}
|
|
89
|
+
className={GHOST_BUTTON}
|
|
90
|
+
aria-label="New side question"
|
|
91
|
+
title="New side question"
|
|
92
|
+
>
|
|
93
|
+
<RefreshCw size={14} strokeWidth={2} />
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
<textarea
|
|
97
|
+
ref={textareaRef}
|
|
98
|
+
rows={1}
|
|
99
|
+
value={value}
|
|
100
|
+
onInput={(event) => onChange(event.currentTarget.value)}
|
|
101
|
+
onKeyDown={handleKeyDown}
|
|
102
|
+
placeholder="Ask your question"
|
|
103
|
+
aria-label="Ask your question"
|
|
104
|
+
className="flex-1 min-w-0 bg-transparent text-[13px] text-ink placeholder-ink/40 resize-none outline-none py-1.5 max-h-32 leading-6"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
{topics.length > 0 && (
|
|
108
|
+
<div className="relative">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
112
|
+
className={GHOST_BUTTON}
|
|
113
|
+
aria-label="Side question history"
|
|
114
|
+
title="Side question history"
|
|
115
|
+
aria-expanded={menuOpen}
|
|
116
|
+
data-btw-menu-anchor=""
|
|
117
|
+
>
|
|
118
|
+
<ChevronDown size={14} strokeWidth={2} />
|
|
119
|
+
</button>
|
|
120
|
+
{menuOpen && (
|
|
121
|
+
<BtwTopicMenu
|
|
122
|
+
topics={topics}
|
|
123
|
+
activeTopicId={activeTopicId}
|
|
124
|
+
onSelectTopic={onSelectTopic}
|
|
125
|
+
onDeleteTopic={onDeleteTopic}
|
|
126
|
+
onCloseMenu={() => setMenuOpen(false)}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{canPromote && (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={onPromote}
|
|
136
|
+
className={GHOST_BUTTON}
|
|
137
|
+
aria-label="Promote answer into chat"
|
|
138
|
+
title="Promote answer into chat"
|
|
139
|
+
>
|
|
140
|
+
<ExternalLink size={14} strokeWidth={2} />
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={onClose}
|
|
147
|
+
className={GHOST_BUTTON}
|
|
148
|
+
aria-label="Close side questions"
|
|
149
|
+
title="Close side questions"
|
|
150
|
+
>
|
|
151
|
+
<X size={14} strokeWidth={2} />
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|