ompchamber 3.1.1 → 3.3.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/index.js +306 -298
- package/dist/client/static/js/index-bsxxt89j.css +1 -0
- package/dist/client/static/js/{index-qhzsybkn.js → index-qkt4z7x0.js} +658 -658
- package/package.json +4 -3
- package/public/sw.js +54 -5
- package/src/cli/lib/output.js +15 -8
- package/src/client/components/common/Modal.tsx +14 -4
- package/src/client/components/{layout/session-sidebar/SubagentList.tsx → common/subagent-list/index.tsx} +8 -10
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +6 -30
- package/src/client/components/layout/session-sidebar/Toolbar.tsx +11 -5
- package/src/client/components/layout/session-sidebar/index.tsx +4 -2
- package/src/client/components/mobile/LayoutWrapper.tsx +6 -0
- package/src/client/components/mobile/SessionSidebar.tsx +4 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +27 -8
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +67 -29
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.test.ts +84 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +59 -12
- package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
- package/src/client/components/settings/Modal.tsx +45 -23
- package/src/client/components/settings/SettingsSidebarList.tsx +1 -1
- package/src/client/components/settings/categories/AgentSettings.tsx +45 -28
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/CommandSettings.tsx +45 -28
- package/src/client/components/settings/categories/McpSettings.tsx +48 -31
- package/src/client/components/settings/categories/ProjectSettings.tsx +34 -19
- package/src/client/components/settings/categories/ProviderSettings.tsx +77 -53
- package/src/client/components/settings/categories/SkillSettings.tsx +34 -17
- package/src/client/components/settings/categories/UsageSettings.tsx +53 -39
- package/src/client/components/settings/categories/appearance-settings/EditorFontSection.tsx +78 -0
- package/src/client/components/settings/categories/behavior-settings/Editor.tsx +7 -7
- package/src/client/components/settings/categories/mcp-settings/ImportModal.tsx +1 -1
- package/src/client/components/settings/categories/mcp-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/project-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +8 -3
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +9 -4
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/skill-catalog/AddSourceModal.tsx +1 -1
- package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/master-detail/index.tsx +68 -0
- package/src/client/components/workspace/SearchPanel.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +7 -1
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +19 -12
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +1 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +2 -1
- package/src/client/components/workspace/diff-panel/SplitView.tsx +31 -5
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +77 -6
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +18 -5
- package/src/client/components/workspace/diff-panel/index.tsx +36 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +11 -4
- package/src/client/components/workspace/editor/index.tsx +15 -7
- package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
- package/src/client/components/workspace/terminal-panel/KeyBar.tsx +194 -0
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +24 -0
- package/src/client/components/workspace/terminal-panel/index.tsx +98 -3
- package/src/client/components/workspace/terminal-panel/mount.ts +47 -0
- package/src/client/data/theme/terminal.ts +25 -8
- package/src/client/hooks/chat/omp/session-list.tsx +31 -1
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +7 -1
- package/src/client/hooks/chat/subagent/index.ts +37 -11
- package/src/client/hooks/editor/font.ts +53 -0
- package/src/client/hooks/editor/use-file-editor.ts +1 -1
- package/src/client/hooks/settings/master-detail.ts +33 -0
- package/src/client/hooks/ui/clipboard.ts +17 -0
- package/src/client/hooks/ui/keyboard-inset.test.ts +46 -0
- package/src/client/hooks/ui/keyboard-inset.ts +113 -0
- package/src/client/hooks/ui/text-overflow.test.ts +152 -0
- package/src/client/hooks/ui/text-overflow.ts +78 -0
- package/src/client/hooks/ui/touch-device.ts +46 -0
- package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
- package/src/client/main.tsx +1 -0
- package/src/server/index.ts +4 -3
- package/src/server/lib/assets/conditional.server.test.ts +74 -0
- package/src/server/lib/assets/conditional.server.ts +54 -0
- package/src/server/lib/assets/dev-assets.server.test.ts +44 -0
- package/src/server/lib/assets/dev-assets.server.ts +121 -0
- package/src/server/lib/assets/fonts.server.ts +9 -4
- package/src/server/lib/bundler/build-errors.server.ts +136 -0
- package/src/server/lib/bundler/build-errors.test.ts +103 -0
- package/src/server/lib/bundler/dependencies.test.ts +134 -0
- package/src/server/lib/db/client.ts +5 -1
- package/src/server/lib/fs/git-diff.ts +34 -10
- package/src/server/lib/fs/git-repos.ts +81 -0
- package/src/server/lib/lifecycle/flock.ts +199 -0
- package/src/server/lib/lifecycle/identity.ts +5 -1
- package/src/server/lib/lifecycle/listener.ts +29 -0
- package/src/server/lib/lifecycle/port-guard.ts +18 -3
- package/src/server/lib/lifecycle/proc/darwin.ts +52 -17
- package/src/server/lib/lifecycle/proc/linux.ts +29 -7
- package/src/server/lib/lifecycle/proc/ps.ts +7 -0
- package/src/server/lib/lifecycle/proc/types.ts +17 -0
- package/src/server/lib/lifecycle/proc/win32.ts +5 -0
- package/src/server/lib/observer-ws.ts +150 -0
- package/src/server/lib/omp/rpc/constants.ts +9 -0
- package/src/server/lib/omp/rpc/subagent-liveness.ts +2 -12
- package/src/server/lib/omp/session/file-scan.ts +7 -1
- package/src/server/lib/omp/session/files.ts +95 -96
- package/src/server/lib/omp/session/locator.ts +18 -8
- package/src/server/lib/omp/session/messages.ts +7 -7
- package/src/server/lib/omp/session/scan.ts +108 -0
- package/src/server/lib/omp/session/stats.ts +50 -50
- package/src/server/lib/omp/session/telemetry/scan.ts +89 -7
- package/src/server/lib/omp/session/telemetry-raw.ts +24 -8
- package/src/server/lib/omp/session/usage/aggregate.ts +170 -55
- package/src/server/lib/terminal/runtime.server.ts +3 -0
- package/src/server/lib/terminal/session.server.ts +30 -3
- package/src/server/plugins/shell.server.ts +52 -13
- package/src/server/plugins/ssr.ts +43 -7
- package/src/server/plugins/static.ts +51 -18
- package/src/server/routes/agent/command.test.ts +69 -0
- package/src/server/routes/agent/command.ts +14 -0
- package/src/server/routes/agent/ws.test.ts +159 -0
- package/src/server/routes/agent/ws.ts +22 -97
- package/src/server/routes/btw/ws.ts +22 -95
- package/src/server/routes/fs/git.ts +61 -70
- package/src/server/routes/omp/session-stats.ts +1 -1
- package/src/server/routes/telemetry/tokens.ts +6 -4
- package/src/server/routes/terminal/run.ts +4 -8
- package/src/server/routes/terminal/ws.ts +24 -2
- package/src/shared/lib/chat/omp/agent-events.test.ts +33 -1
- package/src/shared/lib/chat/omp/agent-events.ts +10 -0
- package/src/shared/lib/code/editor/typography.test.ts +93 -0
- package/src/shared/lib/code/editor/typography.ts +86 -0
- package/src/shared/lib/code/language.ts +146 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +2 -1
- package/src/shared/lib/code/syntax-highlight.ts +0 -122
- package/src/shared/lib/fonts/NOTICE.md +44 -0
- package/src/shared/lib/fonts/nerd-symbols.css +55 -0
- package/src/shared/lib/fonts/nerd-symbols.test.ts +134 -0
- package/src/shared/lib/fonts/symbols-nerd-font-mono.woff2 +0 -0
- package/src/shared/lib/fs/diff-parser.test.ts +5 -5
- package/src/shared/lib/fs/diff-parser.ts +67 -42
- package/src/shared/lib/omp/subagent/parse.test.ts +88 -0
- package/src/shared/lib/omp/subagent/parse.ts +17 -0
- package/src/shared/lib/omp/subagent/transcript-client.test.ts +100 -0
- package/src/shared/lib/omp/subagent/transcript-client.ts +17 -1
- package/src/shared/lib/settings/diff.test.ts +1 -1
- package/src/shared/lib/workspace/terminal/keys.test.ts +189 -0
- package/src/shared/lib/workspace/terminal/keys.ts +225 -0
- package/src/shared/types/omp/subagent.ts +6 -0
- package/src/shared/types/settings/state.ts +7 -16
- package/dist/client/static/js/index-5yfre1kw.css +0 -1
|
@@ -31,7 +31,7 @@ export const McpSidebarList: FunctionComponent<McpSidebarListProps> = ({
|
|
|
31
31
|
const activeProjectName = projects.find((proj) => proj.path === selectedProject)?.name ?? 'Global (all projects)';
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
|
-
<div className="w-
|
|
34
|
+
<div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0 select-none">
|
|
35
35
|
<ProjectSelectorDropdown
|
|
36
36
|
selectedProject={selectedProject}
|
|
37
37
|
onChangeProject={onChangeProject}
|
|
@@ -39,7 +39,7 @@ export function ProjectSidebarList({
|
|
|
39
39
|
onAddProject,
|
|
40
40
|
}: ProjectSidebarListProps) {
|
|
41
41
|
return (
|
|
42
|
-
<div className="w-
|
|
42
|
+
<div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0">
|
|
43
43
|
{/* List Header */}
|
|
44
44
|
<div className="px-3.5 py-3 border-b border-ink/10 flex items-center justify-between">
|
|
45
45
|
<span className="text-xs font-semibold text-ink">
|
|
@@ -31,8 +31,13 @@ export function ProviderAuthSection({
|
|
|
31
31
|
Authentication
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
{/* Wraps instead of overflowing: this row is a status group plus three
|
|
35
|
+
action buttons, which needs ~330px, while the detail pane is 206px
|
|
36
|
+
at a 768px window (the modal's own two-pane split). Without the wrap
|
|
37
|
+
the buttons ran past the panel and were clipped by the modal's
|
|
38
|
+
`overflow-hidden`. */}
|
|
39
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
|
40
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
36
41
|
{isDisabled ? (
|
|
37
42
|
<>
|
|
38
43
|
<Ban size={15} className="text-ink/50 flex-shrink-0" strokeWidth={2.4} />
|
|
@@ -68,7 +73,7 @@ export function ProviderAuthSection({
|
|
|
68
73
|
</div>
|
|
69
74
|
</div>
|
|
70
75
|
|
|
71
|
-
<div className="flex items-center gap-2">
|
|
76
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
72
77
|
<button
|
|
73
78
|
type="button"
|
|
74
79
|
onClick={onToggleDisabled}
|
|
@@ -57,7 +57,7 @@ export function DeleteProviderModal({
|
|
|
57
57
|
aria-label="Confirm provider deletion"
|
|
58
58
|
>
|
|
59
59
|
<div
|
|
60
|
-
className="bg-paper border border-ink/20 rounded-xl shadow-2xl w-[min(94vw,460px)] overflow-
|
|
60
|
+
className="bg-paper border border-ink/20 rounded-xl shadow-2xl w-[min(94vw,460px)] max-h-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static text-ink"
|
|
61
61
|
onClick={(e) => e.stopPropagation()}
|
|
62
62
|
>
|
|
63
63
|
<div className="flex items-center gap-3 px-5 py-4 border-b border-ink/10">
|
|
@@ -42,12 +42,14 @@ export function ProviderModelsList({
|
|
|
42
42
|
return (
|
|
43
43
|
<div className="space-y-3.5">
|
|
44
44
|
{/* 1. Header with count & hide all / show all */}
|
|
45
|
-
<div className="flex items-center justify-between gap-4">
|
|
45
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
|
46
46
|
<div className="text-xs font-semibold text-ink">
|
|
47
47
|
Available Models <span className="font-normal text-ink/60">({models.length})</span>
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
{/* Wraps: four action chips need ~330px, which the detail pane does not
|
|
51
|
+
have in a two-pane modal at a 768-900px window. */}
|
|
52
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
51
53
|
<button
|
|
52
54
|
type="button"
|
|
53
55
|
onClick={onFetchModels}
|
|
@@ -133,8 +135,11 @@ export function ProviderModelsList({
|
|
|
133
135
|
</span>
|
|
134
136
|
</div>
|
|
135
137
|
|
|
136
|
-
{/* Right: Price & Context Badge & Action Icons
|
|
137
|
-
|
|
138
|
+
{/* Right: Price & Context Badge & Action Icons. Wraps rather than
|
|
139
|
+
running past the pane: a long price plus a context badge plus
|
|
140
|
+
three icons exceeds the detail pane's width in a two-pane
|
|
141
|
+
modal on a ~768px window. */}
|
|
142
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
138
143
|
{/* Pricing Badge (per 1M tokens) */}
|
|
139
144
|
{(model.priceInput !== undefined || model.priceOutput !== undefined) && (
|
|
140
145
|
<span
|
|
@@ -35,7 +35,7 @@ export function ProviderSidebarList({
|
|
|
35
35
|
useOnClickOutside(dropdownRef, () => setIsProjectDropdownOpen(false));
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
<div className="w-
|
|
38
|
+
<div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0">
|
|
39
39
|
{/* 1. Project Selector Dropdown */}
|
|
40
40
|
<div className="p-3 border-b border-ink/10 relative" ref={dropdownRef}>
|
|
41
41
|
<button
|
|
@@ -50,7 +50,7 @@ export function AddSourceModal({ isOpen, onClose, onAddSource }: AddSourceModalP
|
|
|
50
50
|
onClick={onClose}
|
|
51
51
|
>
|
|
52
52
|
<div
|
|
53
|
-
className="bg-paper border border-ink/20 rounded-xl max-w-md w-full p-6 shadow-2xl space-y-4 text-ink animate-in fade-in zoom-in-95 duration-150"
|
|
53
|
+
className="bg-paper border border-ink/20 rounded-xl max-w-md w-full p-6 shadow-2xl space-y-4 text-ink animate-in fade-in zoom-in-95 duration-150 max-h-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static"
|
|
54
54
|
onClick={(e) => e.stopPropagation()}
|
|
55
55
|
>
|
|
56
56
|
<div className="flex items-center justify-between pb-3 border-b border-ink/10">
|
|
@@ -49,7 +49,7 @@ export function SkillSidebarList({
|
|
|
49
49
|
const totalCount = skills.length;
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
|
-
<div className="w-
|
|
52
|
+
<div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0">
|
|
53
53
|
{/* Project Selector Dropdown */}
|
|
54
54
|
<ProjectSelectorDropdown
|
|
55
55
|
variant="compact"
|
|
@@ -24,7 +24,7 @@ export function UsageSidebarList({
|
|
|
24
24
|
const providers = buildProviders(report);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className="w-
|
|
27
|
+
<div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0 select-none">
|
|
28
28
|
<div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between">
|
|
29
29
|
<span className="text-xs font-semibold text-ink">
|
|
30
30
|
{providers.length} provider{providers.length === 1 ? '' : 's'} with keys
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentChildren } from 'preact';
|
|
7
|
+
import { ChevronLeft } from 'lucide-preact';
|
|
8
|
+
import type { SettingsPane } from '@/client/hooks/settings/master-detail';
|
|
9
|
+
|
|
10
|
+
interface SettingsMasterDetailProps {
|
|
11
|
+
/** Which pane a phone is showing; desktop shows both. */
|
|
12
|
+
pane: SettingsPane;
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
/** The list pane (a settings sidebar). Owns its own `w-full md:w-64` width. */
|
|
15
|
+
list: ComponentChildren;
|
|
16
|
+
detail: ComponentChildren;
|
|
17
|
+
/** Name of the list, shown on the phone's back bar. */
|
|
18
|
+
listLabel: string;
|
|
19
|
+
/** Extra classes for the root (e.g. a category-wide `text-xs`). */
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Master-detail shell for the settings categories that are two panes on a
|
|
25
|
+
* desktop (providers, projects, agents, commands, MCP, skills, usage).
|
|
26
|
+
*
|
|
27
|
+
* Both panes stay mounted at every width, so a pane's state — a half-typed
|
|
28
|
+
* form, a scroll position — survives the drill and the back. Only the phone
|
|
29
|
+
* needs the extra bar: on `md` the two panes sit side by side exactly as
|
|
30
|
+
* before and the bar is not rendered at all.
|
|
31
|
+
*/
|
|
32
|
+
export function SettingsMasterDetail({
|
|
33
|
+
pane,
|
|
34
|
+
onBack,
|
|
35
|
+
list,
|
|
36
|
+
detail,
|
|
37
|
+
listLabel,
|
|
38
|
+
className = '',
|
|
39
|
+
}: SettingsMasterDetailProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={`flex-1 flex flex-col md:flex-row h-full w-full min-w-0 overflow-hidden bg-paper text-ink ${className}`}>
|
|
42
|
+
{/* `min-w-0` on both panes: a flex item's default `min-width: auto` is its
|
|
43
|
+
content's min-content width, so the detail pane refused to shrink below
|
|
44
|
+
its widest row (a provider's name + action strip) and ran 207px past the
|
|
45
|
+
modal at a 768-900px window — clipped, since the modal is
|
|
46
|
+
`overflow-hidden`. The list pane carries it for the same reason. */}
|
|
47
|
+
<div className={`${pane === 'list' ? 'flex' : 'hidden'} md:flex flex-1 md:flex-none md:w-auto min-h-0 min-w-0 flex-col`}>
|
|
48
|
+
{list}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className={`${pane === 'detail' ? 'flex' : 'hidden'} md:flex flex-1 min-h-0 min-w-0 flex-col`}>
|
|
52
|
+
<div className="md:hidden flex items-center px-2 py-1 border-b border-ink/10 bg-canvas flex-shrink-0">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onBack}
|
|
56
|
+
aria-label={`Back to ${listLabel}`}
|
|
57
|
+
className="flex items-center gap-1 -ml-0.5 pl-0.5 pr-2 py-1 rounded-lg text-ink hover:bg-ink/5 active:bg-ink/10 transition-colors cursor-pointer"
|
|
58
|
+
>
|
|
59
|
+
<ChevronLeft size={18} />
|
|
60
|
+
<span className="text-xs font-semibold truncate">{listLabel}</span>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{detail}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -9,7 +9,8 @@ import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
|
9
9
|
import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
|
|
10
10
|
import { useSearchStream } from '@/client/hooks/workspace/search-stream';
|
|
11
11
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
12
|
-
import { getLanguageFromPath
|
|
12
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
13
|
+
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
13
14
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
14
15
|
|
|
15
16
|
export function SearchPanel({ className = '', enabled = true, rootPath }: { className?: string, enabled?: boolean, rootPath?: string }) {
|
|
@@ -18,6 +18,8 @@ interface SubagentViewProps {
|
|
|
18
18
|
onBack: () => void;
|
|
19
19
|
provider?: string;
|
|
20
20
|
providerNames?: Record<string, string>;
|
|
21
|
+
/** Mobile renders the chat's compact footer variant (three-dot menu). */
|
|
22
|
+
isMobile?: boolean;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -30,7 +32,7 @@ const STATUS_LABEL: Record<SubagentInfo['status'], string> = {
|
|
|
30
32
|
|
|
31
33
|
/** Read-only subagent transcript: compact banner, live-growing message list,
|
|
32
34
|
* and a notice row where the main timeline's composer would be. */
|
|
33
|
-
export function SubagentView({ sessionId, subagent, onBack, provider, providerNames, className = '' }: SubagentViewProps) {
|
|
35
|
+
export function SubagentView({ sessionId, subagent, onBack, provider, providerNames, isMobile = false, className = '' }: SubagentViewProps) {
|
|
34
36
|
const { messages, isLoading, status, isActive } = useSubagentTranscript(sessionId, subagent, onBack);
|
|
35
37
|
const modelNames = useModelNames();
|
|
36
38
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -133,6 +135,7 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
133
135
|
msg={msg}
|
|
134
136
|
isStreaming={isLoading}
|
|
135
137
|
isPrevAssistant={isPrevAssistant}
|
|
138
|
+
userActions={false}
|
|
136
139
|
className={isNoticeRow(msg) ? 'mt-3 mb-1' : isPrevAssistant ? 'mt-1' : 'mt-3'}
|
|
137
140
|
/>
|
|
138
141
|
{footer && (
|
|
@@ -142,7 +145,10 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
142
145
|
providerNames={providerNames}
|
|
143
146
|
modelName={modelName}
|
|
144
147
|
modelNames={modelNames}
|
|
148
|
+
thinkingLevel={progress?.resolvedThinkingLevel}
|
|
145
149
|
durationMs={footer.durationMs}
|
|
150
|
+
isMobile={isMobile}
|
|
151
|
+
showActions={false}
|
|
146
152
|
/>
|
|
147
153
|
)}
|
|
148
154
|
</Fragment>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useMemo, useState } from 'preact/hooks';
|
|
1
|
+
import { useMemo, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { AlertCircle, Bell, Bot, CheckCircle2, ChevronDown, Clock, Info, Layers } from 'lucide-preact';
|
|
3
|
-
import {
|
|
3
|
+
import { isCodeLike } from '@/shared/lib/code/language';
|
|
4
|
+
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
4
5
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
6
|
+
import { useIsTruncated } from '@/client/hooks/ui/text-overflow';
|
|
5
7
|
import { parseTaskNotice } from '@/shared/lib/chat/task-result-parser';
|
|
6
8
|
import { TaskResultContent } from '@/client/components/workspace/chat-timeline/TaskResultContent';
|
|
7
9
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
@@ -59,12 +61,17 @@ export function SystemNotice({ notice }: SystemNoticeProps) {
|
|
|
59
61
|
}, [notice]);
|
|
60
62
|
|
|
61
63
|
const rawTitle = taskNotice?.intro || reminderInfo?.title || genericInfo.firstLine || notice;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// The subtitle is clipped by CSS at the card's own width, so the decision to
|
|
65
|
+
// offer an expander is a MEASUREMENT, not a character count: 76 characters
|
|
66
|
+
// fit a desktop timeline and are cut on a phone card. A char threshold left
|
|
67
|
+
// those rows showing an ellipsis with nothing to click (the chevron was
|
|
68
|
+
// dropped with the button disabled). Measured on the exact node that carries
|
|
69
|
+
// `truncate`, so it cannot disagree with the ellipsis the user sees.
|
|
70
|
+
const subtitleRef = useRef<HTMLSpanElement>(null);
|
|
71
|
+
const subtitleClipped = useIsTruncated(subtitleRef, rawTitle);
|
|
64
72
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// If it exceeds the ellipsis limit (>100 chars) or has multiple lines, keep expand active.
|
|
73
|
+
// Expandable when the subtitle is visually clipped, or when the body holds
|
|
74
|
+
// something the one-line header cannot show (more lines, a task-result card).
|
|
68
75
|
const isExpandable = useMemo(() => {
|
|
69
76
|
if (taskNotice) return true;
|
|
70
77
|
const clean = notice
|
|
@@ -74,8 +81,8 @@ export function SystemNotice({ notice }: SystemNoticeProps) {
|
|
|
74
81
|
.split(/\r?\n/)
|
|
75
82
|
.map((l) => l.trim())
|
|
76
83
|
.filter(Boolean);
|
|
77
|
-
return lines.length > 1 ||
|
|
78
|
-
}, [taskNotice, notice,
|
|
84
|
+
return lines.length > 1 || subtitleClipped;
|
|
85
|
+
}, [taskNotice, notice, subtitleClipped]);
|
|
79
86
|
|
|
80
87
|
const noticeStyle = useMemo(() => {
|
|
81
88
|
if (taskNotice) {
|
|
@@ -168,9 +175,9 @@ export function SystemNotice({ notice }: SystemNoticeProps) {
|
|
|
168
175
|
</span>
|
|
169
176
|
)}
|
|
170
177
|
</span>
|
|
171
|
-
{!isOpen &&
|
|
172
|
-
<span className="truncate font-mono text-[10.5px] text-ink/45" title={rawTitle}>
|
|
173
|
-
{
|
|
178
|
+
{!isOpen && rawTitle && (
|
|
179
|
+
<span ref={subtitleRef} className="truncate font-mono text-[10.5px] text-ink/45" title={rawTitle}>
|
|
180
|
+
{rawTitle}
|
|
174
181
|
</span>
|
|
175
182
|
)}
|
|
176
183
|
</span>
|
|
@@ -3,7 +3,8 @@ import { AlignLeft, Code, Eye, FileCode, FileText, Layers, Terminal } from 'luci
|
|
|
3
3
|
import type { ParsedTaskNotice } from '@/shared/lib/chat/task-result-parser';
|
|
4
4
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
5
5
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
6
|
-
import {
|
|
6
|
+
import { isCodeLike } from '@/shared/lib/code/language';
|
|
7
|
+
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
7
8
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
8
9
|
|
|
9
10
|
interface TaskResultContentProps {
|
package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { useMemo } from 'preact/hooks';
|
|
14
14
|
import type { HashlineSection, HashlineVerb } from '@/shared/lib/omp/session/hashline-patch';
|
|
15
|
-
import { getLanguageFromPath
|
|
15
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
16
|
+
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
16
17
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
17
18
|
|
|
18
19
|
/** A hashline `CUT` removes lines the model already saw, so it reads as the
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
2
|
import { ArrowRight, CheckCircle2, Clock, Wand2 } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
|
-
import { getLanguageFromPath
|
|
4
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
5
|
+
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
5
6
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
6
7
|
|
|
7
8
|
interface AstEditItem {
|
|
@@ -3,7 +3,8 @@ import { Check, Clock } from 'lucide-preact';
|
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
5
|
import { stripAnsiCodes } from '@/shared/lib/code/ansi';
|
|
6
|
-
import {
|
|
6
|
+
import { isCodeLike } from '@/shared/lib/code/language';
|
|
7
|
+
import { highlightCode, tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
7
8
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
8
9
|
import { JsonCodeBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock';
|
|
9
10
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
@@ -6,7 +6,8 @@ import { DiffView } from '@/client/components/workspace/chat-timeline/tool-rende
|
|
|
6
6
|
import { HashlinePatch } from '@/client/components/workspace/chat-timeline/tool-renderers/hashline-patch';
|
|
7
7
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
8
8
|
import { EnvelopeHeader } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader';
|
|
9
|
-
import { getLanguageFromPath
|
|
9
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
10
|
+
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
10
11
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
11
12
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
12
13
|
import { outputMarkdown, readToolOutput } from '@/shared/lib/chat/tool-output';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
2
|
import { FileCode, FileImage, FileText, Folder, FolderOpen, Info, Loader2 } from 'lucide-preact';
|
|
3
3
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
4
|
-
import { getLanguageFromPath
|
|
4
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
5
|
+
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
5
6
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
6
7
|
import { parseDirListing, parseNumberedCode } from '@/shared/lib/code/parser';
|
|
7
8
|
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
@@ -2,7 +2,8 @@ import { useMemo, useState } from 'preact/hooks';
|
|
|
2
2
|
import { ChevronRight, FileSearch, FileText, FolderSearch } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
-
import { getLanguageFromPath
|
|
5
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
6
|
+
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
6
7
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
7
8
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
8
9
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import { getLanguageFromPath
|
|
2
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
3
|
+
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
3
4
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
4
5
|
|
|
5
6
|
interface DiffLine {
|
|
@@ -6,9 +6,11 @@ import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
|
6
6
|
interface SplitViewProps {
|
|
7
7
|
rows: SplitDiffRow[];
|
|
8
8
|
language: string;
|
|
9
|
+
/** Wrap long lines instead of scrolling horizontally. */
|
|
10
|
+
wordWrap: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function SplitView({ rows, language }: SplitViewProps) {
|
|
13
|
+
export function SplitView({ rows, language, wordWrap }: SplitViewProps) {
|
|
12
14
|
const syntaxReady = useSyntaxReady();
|
|
13
15
|
|
|
14
16
|
const htmlByIndex = useMemo(() => {
|
|
@@ -47,7 +49,7 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
47
49
|
{/* The room under the last row belongs to the content, not the scroller:
|
|
48
50
|
a scroll container's own bottom padding is not part of its scrollable
|
|
49
51
|
overflow, so the padding has to sit inside it to have any effect. */}
|
|
50
|
-
<div className=
|
|
52
|
+
<div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full min-w-[700px]'}`}>
|
|
51
53
|
<div className="grid grid-cols-2 sticky top-0 z-10 bg-canvas border-b border-ink/10 text-[11px] text-ink/70 font-sans select-none shadow-xs">
|
|
52
54
|
<div className="px-3 py-1 font-medium border-r border-ink/10 flex items-center justify-between">
|
|
53
55
|
<span>Original (HEAD / Base)</span>
|
|
@@ -57,7 +59,23 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
57
59
|
</div>
|
|
58
60
|
</div>
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
{/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
|
|
63
|
+
cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
|
|
64
|
+
lower it), so a long line would widen the table and scroll instead of
|
|
65
|
+
wrapping. Fixed layout takes its widths from the FIRST row — which
|
|
66
|
+
here is the `colSpan={4}` meta row, so every column came out equal
|
|
67
|
+
(measured 99px) and the line-number gutters grew to the width of a
|
|
68
|
+
code column. The colgroup states them instead: two 48px gutters and
|
|
69
|
+
the two content columns sharing the rest. */}
|
|
70
|
+
<table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-full'}`}>
|
|
71
|
+
{wordWrap && (
|
|
72
|
+
<colgroup>
|
|
73
|
+
<col className="w-12" />
|
|
74
|
+
<col />
|
|
75
|
+
<col className="w-12" />
|
|
76
|
+
<col />
|
|
77
|
+
</colgroup>
|
|
78
|
+
)}
|
|
61
79
|
<tbody>
|
|
62
80
|
{rows.map((row, idx) => {
|
|
63
81
|
if (row.isMeta) {
|
|
@@ -81,6 +99,8 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
81
99
|
{/* Left Line Number */}
|
|
82
100
|
<td
|
|
83
101
|
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
102
|
+
wordWrap ? 'align-top' : ''
|
|
103
|
+
} ${
|
|
84
104
|
isLeftDel
|
|
85
105
|
? 'bg-error/15 text-error font-semibold'
|
|
86
106
|
: left
|
|
@@ -93,7 +113,9 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
93
113
|
|
|
94
114
|
{/* Left Content (Original) */}
|
|
95
115
|
<td
|
|
96
|
-
className={`
|
|
116
|
+
className={`py-0.5 px-2 border-r border-ink/10 text-ink ${
|
|
117
|
+
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
118
|
+
} ${
|
|
97
119
|
isLeftDel
|
|
98
120
|
? 'bg-error/10'
|
|
99
121
|
: left
|
|
@@ -121,6 +143,8 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
121
143
|
{/* Right Line Number */}
|
|
122
144
|
<td
|
|
123
145
|
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
146
|
+
wordWrap ? 'align-top' : ''
|
|
147
|
+
} ${
|
|
124
148
|
isRightAdd
|
|
125
149
|
? 'bg-success/15 text-success font-semibold'
|
|
126
150
|
: right
|
|
@@ -133,7 +157,9 @@ export function SplitView({ rows, language }: SplitViewProps) {
|
|
|
133
157
|
|
|
134
158
|
{/* Right Content (Modified) */}
|
|
135
159
|
<td
|
|
136
|
-
className={`
|
|
160
|
+
className={`py-0.5 px-2 text-ink ${
|
|
161
|
+
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
162
|
+
} ${
|
|
137
163
|
isRightAdd
|
|
138
164
|
? 'bg-success/10'
|
|
139
165
|
: right
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import { AlignJustify, Check, Code2, Columns, Copy, Minus, Plus, RefreshCw, Space, Undo2 } from 'lucide-preact';
|
|
1
|
+
import { AlignJustify, Check, Code2, Columns, Copy, Diff, FileCode, Minus, Plus, RefreshCw, Space, Undo2, WrapText } from 'lucide-preact';
|
|
2
2
|
import { FileIcon } from '@/client/components/common/file-icon';
|
|
3
3
|
import { getGitStatusInfo } from '@/shared/lib/fs/git-status';
|
|
4
4
|
|
|
5
5
|
export type DiffViewMode = 'unified' | 'split';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* How much of the file the diff shows. `full` is the default: the whole file
|
|
9
|
+
* with the changes marked inside it, which is what a reader wants when the
|
|
10
|
+
* hunks alone leave the edit without context. `diff` asks git for its own
|
|
11
|
+
* default context and shows only the changed regions.
|
|
12
|
+
*/
|
|
13
|
+
export type DiffContentMode = 'full' | 'diff';
|
|
14
|
+
|
|
7
15
|
interface DiffToolbarProps {
|
|
8
16
|
filePath: string;
|
|
9
17
|
status: string;
|
|
10
18
|
isStaged: boolean;
|
|
11
19
|
viewMode: DiffViewMode;
|
|
20
|
+
contentMode: DiffContentMode;
|
|
21
|
+
wordWrap: boolean;
|
|
12
22
|
ignoreWhitespace: boolean;
|
|
13
23
|
additions: number;
|
|
14
24
|
deletions: number;
|
|
@@ -17,6 +27,8 @@ interface DiffToolbarProps {
|
|
|
17
27
|
/** A stage/unstage/discard write is in flight; the toolbar locks its actions. */
|
|
18
28
|
isBusy?: boolean;
|
|
19
29
|
onSetViewMode: (mode: DiffViewMode) => void;
|
|
30
|
+
onSetContentMode: (mode: DiffContentMode) => void;
|
|
31
|
+
onToggleWordWrap: () => void;
|
|
20
32
|
onToggleWhitespace: () => void;
|
|
21
33
|
onStageUnstage: () => void;
|
|
22
34
|
onDiscard: () => void;
|
|
@@ -33,6 +45,8 @@ export function DiffToolbar({
|
|
|
33
45
|
status,
|
|
34
46
|
isStaged,
|
|
35
47
|
viewMode,
|
|
48
|
+
contentMode,
|
|
49
|
+
wordWrap,
|
|
36
50
|
ignoreWhitespace,
|
|
37
51
|
additions,
|
|
38
52
|
deletions,
|
|
@@ -40,6 +54,8 @@ export function DiffToolbar({
|
|
|
40
54
|
isCopied,
|
|
41
55
|
isBusy = false,
|
|
42
56
|
onSetViewMode,
|
|
57
|
+
onSetContentMode,
|
|
58
|
+
onToggleWordWrap,
|
|
43
59
|
onToggleWhitespace,
|
|
44
60
|
onStageUnstage,
|
|
45
61
|
onDiscard,
|
|
@@ -73,11 +89,15 @@ export function DiffToolbar({
|
|
|
73
89
|
</div>
|
|
74
90
|
|
|
75
91
|
{/* Right: Controls & actions. `ml-auto` keeps them right-aligned where the
|
|
76
|
-
identity block is hidden. This strip
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
identity block is hidden. This strip WRAPS rather than scrolls: a
|
|
93
|
+
320px panel cannot hold ten actions in one row, and the previous
|
|
94
|
+
`overflow-x-auto no-scrollbar` clipped the overflow invisibly —
|
|
95
|
+
measured 361px of content in a 296px box, with `Stage` entirely
|
|
96
|
+
outside it and no scrollbar to say so. Wrapping costs one extra row on
|
|
97
|
+
a narrow panel and keeps every action reachable. Each segment sets its
|
|
98
|
+
OWN mode — toggling meant clicking the already-active segment flipped
|
|
99
|
+
the view to the other one. */}
|
|
100
|
+
<div className="flex flex-wrap items-center justify-end gap-x-0.5 @[440px]:gap-x-1 gap-y-1 text-ink/70 ml-auto">
|
|
81
101
|
{/* Change counts live here, not in the identity block: the editor tab
|
|
82
102
|
carries the file name but never the counts, so this is the one piece
|
|
83
103
|
of the header that must survive a narrow panel. */}
|
|
@@ -118,6 +138,57 @@ export function DiffToolbar({
|
|
|
118
138
|
</button>
|
|
119
139
|
</div>
|
|
120
140
|
|
|
141
|
+
{/* Content scope toggle (Full Code vs Diff Only). A separate group from
|
|
142
|
+
the view-mode toggle because the two are orthogonal: Unified/Split is
|
|
143
|
+
the shape of the rows, Full/Diff is how many of them there are. Each
|
|
144
|
+
segment SETS its mode — clicking the active one must not flip the
|
|
145
|
+
other, which is what a toggle would do. */}
|
|
146
|
+
<div className="flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-0.5 @[440px]:mr-1 flex-shrink-0">
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => onSetContentMode('full')}
|
|
150
|
+
className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
|
|
151
|
+
contentMode === 'full' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
|
|
152
|
+
}`}
|
|
153
|
+
title="Full code (whole file, changes marked)"
|
|
154
|
+
aria-label="Full code (whole file, changes marked)"
|
|
155
|
+
aria-pressed={contentMode === 'full'}
|
|
156
|
+
>
|
|
157
|
+
<FileCode size={12} />
|
|
158
|
+
<span className="hidden @[420px]:inline">Full Code</span>
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => onSetContentMode('diff')}
|
|
163
|
+
className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
|
|
164
|
+
contentMode === 'diff' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
|
|
165
|
+
}`}
|
|
166
|
+
title="Diff only (changed hunks)"
|
|
167
|
+
aria-label="Diff only (changed hunks)"
|
|
168
|
+
aria-pressed={contentMode === 'diff'}
|
|
169
|
+
>
|
|
170
|
+
<Diff size={12} />
|
|
171
|
+
<span className="hidden @[420px]:inline">Diff Only</span>
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Word wrap. On by default: a diff of a minified file or a long line
|
|
176
|
+
is unreadable when it runs off the panel, and horizontal scrolling
|
|
177
|
+
hides the change the reader opened the diff to see. Off keeps one
|
|
178
|
+
line = one row, which is what a column-aligned comparison needs. */}
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
onClick={onToggleWordWrap}
|
|
182
|
+
className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
|
|
183
|
+
wordWrap ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
|
|
184
|
+
}`}
|
|
185
|
+
title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
|
|
186
|
+
aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
|
|
187
|
+
aria-pressed={wordWrap}
|
|
188
|
+
>
|
|
189
|
+
<WrapText size={13} />
|
|
190
|
+
</button>
|
|
191
|
+
|
|
121
192
|
{/* Ignore whitespace toggle */}
|
|
122
193
|
<button
|
|
123
194
|
type="button"
|