@cueloop/client 0.1.0-alpha.72 → 0.1.0-alpha.74
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/package.json +5 -5
- package/src/App.tsx +35 -170
- package/src/App.workbench.test.tsx +103 -2
- package/src/app-screens.tsx +84 -10
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.tsx +32 -6
- package/src/components/BareWorkbenchFileView.tsx +1 -1
- package/src/components/ConfirmCard.tsx +0 -1
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +165 -0
- package/src/components/SettingsDialog.tsx +4 -13
- package/src/components/ThreadTree.tsx +6 -18
- package/src/components/__snapshots__/stories.test.tsx.snap +112 -68
- package/src/components/primitives/Card.tsx +1 -1
- package/src/components/primitives/frame.test.ts +5 -5
- package/src/components/primitives/frame.ts +5 -7
- package/src/components/quick-actions-editor.stories.tsx +2 -0
- package/src/components/quick-actions-editor.test.tsx +36 -2
- package/src/components/quick-actions-editor.tsx +104 -29
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config.test.ts +36 -0
- package/src/config.ts +38 -8
- package/src/index.ts +2 -0
- package/src/intent-dispatch.test.ts +1 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -0
- package/src/run.ts +10 -0
- package/src/serve.ts +31 -0
- package/src/thread-controller.live-diff.test.ts +119 -0
- package/src/thread-controller.ts +88 -27
- package/src/use-changes-workbench.ts +41 -4
- package/src/use-remember-layout.ts +31 -0
- package/src/use-settings-dialog.tsx +11 -2
- package/src/view-diff.test.ts +73 -2
- package/src/view-diff.ts +38 -0
- package/src/workbench-snapshot.test.ts +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/components/fade-title.test.ts +0 -49
- package/src/components/fade-title.ts +0 -61
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
// always present when the region is on, Changes rides on top of it, and a thin rail holds the sidebar
|
|
6
6
|
// toggle when the region is closed. Each pane owns its own header controls; the thread header never does.
|
|
7
7
|
|
|
8
|
-
import React from "react";
|
|
8
|
+
import React, { useRef } from "react";
|
|
9
9
|
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import type { BoxRenderable } from "@opentui/core";
|
|
10
11
|
import { DARK, type Theme } from "../theme";
|
|
12
|
+
import { useFrameMeasure } from "../use-frame-measure";
|
|
13
|
+
import { truncateTitle } from "./truncate-title";
|
|
11
14
|
import { PanelColumn } from "./PanelColumn";
|
|
12
15
|
import { IconButton } from "./primitives/IconButton";
|
|
13
16
|
import { NERD, HEADER_UNDERLINE_CHARS } from "./primitives/icons";
|
|
@@ -16,6 +19,25 @@ import { RootOverlayProvider } from "./RootOverlay";
|
|
|
16
19
|
|
|
17
20
|
export type ProjectPanelMode = "changes" | "tree";
|
|
18
21
|
|
|
22
|
+
/** The thread title on one line: it shrinks with the header and tails off in an ellipsis rather than wrapping. */
|
|
23
|
+
function HeaderTitle({ title, color }: { title: string; color: string }): React.ReactNode {
|
|
24
|
+
const boxRef = useRef<BoxRenderable | null>(null);
|
|
25
|
+
const width = useFrameMeasure(
|
|
26
|
+
() => boxRef.current?.width ?? 0,
|
|
27
|
+
(left, right) => left === right,
|
|
28
|
+
0,
|
|
29
|
+
);
|
|
30
|
+
const clipped = width > 0 ? truncateTitle(title, width) : title;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<box ref={boxRef} style={{ flexShrink: 1, minWidth: 0 }}>
|
|
34
|
+
<text fg={color} wrapMode="none">
|
|
35
|
+
{clipped}
|
|
36
|
+
</text>
|
|
37
|
+
</box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
export interface AppShellProps {
|
|
20
42
|
sidebarOpen: boolean;
|
|
21
43
|
onToggleSidebar: () => void;
|
|
@@ -158,7 +180,7 @@ export function AppShell({
|
|
|
158
180
|
<IconButton
|
|
159
181
|
glyph={NERD.sidebarRight}
|
|
160
182
|
onPress={onToggleRight}
|
|
161
|
-
tip="Toggle
|
|
183
|
+
tip="Toggle Sidebar"
|
|
162
184
|
theme={tokens}
|
|
163
185
|
/>
|
|
164
186
|
</box>
|
|
@@ -172,7 +194,7 @@ export function AppShell({
|
|
|
172
194
|
<IconButton
|
|
173
195
|
glyph={NERD.sidebarRightOff}
|
|
174
196
|
onPress={onToggleRight}
|
|
175
|
-
tip="Toggle
|
|
197
|
+
tip="Toggle Sidebar"
|
|
176
198
|
marginLeft={1}
|
|
177
199
|
theme={tokens}
|
|
178
200
|
/>
|
|
@@ -200,9 +222,13 @@ export function AppShell({
|
|
|
200
222
|
<PanelColumn
|
|
201
223
|
width={threadPaneWidth}
|
|
202
224
|
header={
|
|
203
|
-
<box style={{ flexDirection: "row" }}>
|
|
204
|
-
{!sidebarOpen ?
|
|
205
|
-
|
|
225
|
+
<box style={{ flexDirection: "row", minWidth: 0 }}>
|
|
226
|
+
{!sidebarOpen ? (
|
|
227
|
+
<box style={{ paddingRight: 2, flexShrink: 0 }}>{brandChrome}</box>
|
|
228
|
+
) : null}
|
|
229
|
+
{threadTitle ? (
|
|
230
|
+
<HeaderTitle title={threadTitle} color={tokens.textDim} />
|
|
231
|
+
) : null}
|
|
206
232
|
</box>
|
|
207
233
|
}
|
|
208
234
|
// with the region closed, the reopen toggle rides the thread header instead of an empty column
|
|
@@ -15,7 +15,7 @@ export interface WorkbenchCommenter {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** An empty, unsaved thread: it holds no notes, just enough shape for the annotation surface. */
|
|
18
|
-
function draftThread(): Thread {
|
|
18
|
+
export function draftThread(): Thread {
|
|
19
19
|
return {
|
|
20
20
|
schemaVersion: SCHEMA_VERSION,
|
|
21
21
|
id: "workbench-draft",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
4
|
+
import type { Story, StoryMeta } from "./story";
|
|
5
|
+
import { GridTabContent, type DiffSurfaceProps } from "./GridTabContent";
|
|
6
|
+
import { fixtureDiffRows, fixtureDiffSession } from "./story-fixtures";
|
|
7
|
+
import { changesTab, fileTab } from "./editor-grid";
|
|
8
|
+
|
|
9
|
+
export const meta: StoryMeta = { title: "Surfaces/GridTabContent" };
|
|
10
|
+
|
|
11
|
+
const noop = (): void => {};
|
|
12
|
+
|
|
13
|
+
function surface(): DiffSurfaceProps {
|
|
14
|
+
return {
|
|
15
|
+
session: fixtureDiffSession({ annotations: [] }),
|
|
16
|
+
quickActions: DEFAULT_QUICK_ACTIONS,
|
|
17
|
+
observer: false,
|
|
18
|
+
onAnnotate: noop,
|
|
19
|
+
onReply: noop,
|
|
20
|
+
onUpdateAnnotation: noop,
|
|
21
|
+
onExit: noop,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The whole-diff Changes tab: every changed file in one scroll container. */
|
|
26
|
+
export const ChangesTab: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<GridTabContent
|
|
29
|
+
tab={changesTab()}
|
|
30
|
+
rows={fixtureDiffRows()}
|
|
31
|
+
surface={surface()}
|
|
32
|
+
rejectedRows={new Set()}
|
|
33
|
+
dimmed={false}
|
|
34
|
+
readFile={async () => null}
|
|
35
|
+
onAddFileComment={noop}
|
|
36
|
+
theme={DARK}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
expectedColors: [DARK.text],
|
|
40
|
+
size: { width: 80, height: 18 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A single-file diff tab: that file's rows alone, indexed on their own. */
|
|
44
|
+
export const FileDiffTab: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<GridTabContent
|
|
47
|
+
tab={fileTab("store.ts", "src/store.ts", "diff")}
|
|
48
|
+
rows={fixtureDiffRows()}
|
|
49
|
+
surface={surface()}
|
|
50
|
+
rejectedRows={new Set()}
|
|
51
|
+
dimmed={false}
|
|
52
|
+
readFile={async () => null}
|
|
53
|
+
onAddFileComment={noop}
|
|
54
|
+
theme={DARK}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
expectedColors: [DARK.text],
|
|
58
|
+
size: { width: 80, height: 18 },
|
|
59
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body an editor tab paints: the whole working-tree/artifact diff for the
|
|
3
|
+
* Changes tab, or a single file's diff (or read-only contents) for a file tab.
|
|
4
|
+
* Shared by the thread workbench and the bare-launch shell so both render a
|
|
5
|
+
* changed file the same way, with comments routed back through one surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { type Anchor } from "@cueloop/schema";
|
|
10
|
+
import { dimmedTheme, type Theme } from "../theme";
|
|
11
|
+
import { changesMarks, type DiffRow } from "../view-diff";
|
|
12
|
+
import {
|
|
13
|
+
DiffContentView,
|
|
14
|
+
type DiffFoldControls,
|
|
15
|
+
type DiffContentViewProps,
|
|
16
|
+
} from "./DiffContentView";
|
|
17
|
+
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
18
|
+
import type { EditorTab } from "./editor-grid";
|
|
19
|
+
|
|
20
|
+
/** The inline-commenting props the diff sheet shares with the thread view, wired once by the app. */
|
|
21
|
+
export type DiffSurfaceProps = Pick<
|
|
22
|
+
DiffContentViewProps,
|
|
23
|
+
| "session"
|
|
24
|
+
| "quickActions"
|
|
25
|
+
| "observer"
|
|
26
|
+
| "commentsEnabled"
|
|
27
|
+
| "resolved"
|
|
28
|
+
| "suspended"
|
|
29
|
+
| "onComposingChange"
|
|
30
|
+
| "onObserverBlocked"
|
|
31
|
+
| "onCursorChange"
|
|
32
|
+
| "focusedAnnotationId"
|
|
33
|
+
| "onFocusAnnotation"
|
|
34
|
+
| "onAnnotate"
|
|
35
|
+
| "onReply"
|
|
36
|
+
| "onUpdateAnnotation"
|
|
37
|
+
| "onExit"
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
/** The Changes tab body: the whole diff in one scroll container, or a bare hint when nothing changed. */
|
|
41
|
+
export function ChangesTabBody(props: {
|
|
42
|
+
rows: DiffRow[];
|
|
43
|
+
surface: DiffSurfaceProps;
|
|
44
|
+
rejectedRows: Set<number>;
|
|
45
|
+
fold?: DiffFoldControls;
|
|
46
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
47
|
+
split?: boolean;
|
|
48
|
+
dimmed: boolean;
|
|
49
|
+
theme: Theme;
|
|
50
|
+
}): React.ReactNode {
|
|
51
|
+
const session = props.surface.session;
|
|
52
|
+
const marks = useMemo(
|
|
53
|
+
() => changesMarks(session, props.rows, props.surface.focusedAnnotationId),
|
|
54
|
+
[session, props.rows, props.surface.focusedAnnotationId],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (props.rows.length === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
60
|
+
<text fg={props.theme.textDim}>No changes</text>
|
|
61
|
+
</box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<DiffContentView
|
|
67
|
+
rows={props.rows}
|
|
68
|
+
marks={marks}
|
|
69
|
+
{...props.surface}
|
|
70
|
+
rejectedRows={props.rejectedRows}
|
|
71
|
+
fold={props.fold}
|
|
72
|
+
fileStats={props.fileStats}
|
|
73
|
+
split={props.split}
|
|
74
|
+
theme={props.dimmed ? dimmedTheme(props.theme) : undefined}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** An editor tab's body: the whole diff for the Changes tab, a file's diff or contents for a file tab. */
|
|
80
|
+
export function GridTabContent(props: {
|
|
81
|
+
tab: EditorTab;
|
|
82
|
+
rows: DiffRow[];
|
|
83
|
+
surface: DiffSurfaceProps;
|
|
84
|
+
rejectedRows: Set<number>;
|
|
85
|
+
fold?: DiffFoldControls;
|
|
86
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
87
|
+
split?: boolean;
|
|
88
|
+
dimmed: boolean;
|
|
89
|
+
readFile: (path: string) => Promise<string | null>;
|
|
90
|
+
/** Persist a comment on a project file; the file view built the anchor against its own lines. */
|
|
91
|
+
onAddFileComment: (path: string, anchor: Anchor, body: string) => void;
|
|
92
|
+
theme: Theme;
|
|
93
|
+
}): React.ReactNode {
|
|
94
|
+
const { tab } = props;
|
|
95
|
+
if (tab.kind === "file" && tab.fileView === "contents" && tab.path !== undefined) {
|
|
96
|
+
const filePath = tab.path;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<AnnotatableFileView
|
|
100
|
+
path={filePath}
|
|
101
|
+
loadContents={props.readFile}
|
|
102
|
+
session={props.surface.session}
|
|
103
|
+
quickActions={props.surface.quickActions}
|
|
104
|
+
observer={props.surface.observer}
|
|
105
|
+
resolved={props.surface.resolved}
|
|
106
|
+
suspended={props.surface.suspended}
|
|
107
|
+
focusedAnnotationId={props.surface.focusedAnnotationId}
|
|
108
|
+
onFocusAnnotation={props.surface.onFocusAnnotation}
|
|
109
|
+
onComposingChange={props.surface.onComposingChange}
|
|
110
|
+
onCursorChange={props.surface.onCursorChange}
|
|
111
|
+
onObserverBlocked={props.surface.onObserverBlocked}
|
|
112
|
+
onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
|
|
113
|
+
onReply={props.surface.onReply}
|
|
114
|
+
onUpdateAnnotation={props.surface.onUpdateAnnotation}
|
|
115
|
+
onExit={props.surface.onExit}
|
|
116
|
+
theme={props.theme}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (tab.kind !== "file") {
|
|
121
|
+
return (
|
|
122
|
+
<ChangesTabBody
|
|
123
|
+
rows={props.rows}
|
|
124
|
+
surface={props.surface}
|
|
125
|
+
rejectedRows={props.rejectedRows}
|
|
126
|
+
fold={props.fold}
|
|
127
|
+
fileStats={props.fileStats}
|
|
128
|
+
split={props.split}
|
|
129
|
+
dimmed={props.dimmed}
|
|
130
|
+
theme={props.theme}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// a single-file tab shows that file's rows alone, so its row indices are its own: comments and
|
|
135
|
+
// the caret report back in whole-diff indices, and the fold controls (a band to fold) do not apply
|
|
136
|
+
const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
|
|
137
|
+
const rows = fileRowIndices.map((index) => props.rows[index]!);
|
|
138
|
+
const wholeIndex = (rowIndex: number): number => fileRowIndices[rowIndex] ?? rowIndex;
|
|
139
|
+
const rejectedRows = new Set(
|
|
140
|
+
fileRowIndices.flatMap((index, rowIndex) => (props.rejectedRows.has(index) ? [rowIndex] : [])),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<ChangesTabBody
|
|
145
|
+
rows={rows}
|
|
146
|
+
surface={{
|
|
147
|
+
...props.surface,
|
|
148
|
+
onCursorChange: (rowIndex) => props.surface.onCursorChange?.(wholeIndex(rowIndex)),
|
|
149
|
+
onAnnotate: (span, body) =>
|
|
150
|
+
props.surface.onAnnotate(
|
|
151
|
+
{
|
|
152
|
+
start: { blockIndex: wholeIndex(span.start.blockIndex), char: span.start.char },
|
|
153
|
+
end: { blockIndex: wholeIndex(span.end.blockIndex), char: span.end.char },
|
|
154
|
+
},
|
|
155
|
+
body,
|
|
156
|
+
),
|
|
157
|
+
}}
|
|
158
|
+
rejectedRows={rejectedRows}
|
|
159
|
+
fileStats={props.fileStats}
|
|
160
|
+
split={props.split}
|
|
161
|
+
dimmed={props.dimmed}
|
|
162
|
+
theme={props.theme}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The settings overlay: a centered Dialog whose left nav
|
|
3
|
-
*
|
|
2
|
+
* The settings overlay: a centered Dialog whose left nav lists the categories as
|
|
3
|
+
* first-level leaves (General, Appearance, Actions, Keybinds), and whose right
|
|
4
4
|
* body shows the active category's typed rows or the keybinds cheatsheet. The
|
|
5
5
|
* component is controlled: the caller owns the open flag, the active
|
|
6
6
|
* category/row/zone, and value changes, so the app's keyboard grammar (and the
|
|
@@ -48,17 +48,8 @@ export interface SettingsDialogProps {
|
|
|
48
48
|
theme?: Theme;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** The left nav tree: a Settings group over its categories, then Keybinds as a leaf. */
|
|
52
51
|
function navTree(categories: SettingsCategory[]): TreeNode[] {
|
|
53
|
-
|
|
54
|
-
.filter((category) => category.id !== KEYBINDS_CATEGORY_ID)
|
|
55
|
-
.map((category) => ({ id: category.id, label: category.name }));
|
|
56
|
-
const nodes: TreeNode[] = [{ id: "settings", label: "Settings", children }];
|
|
57
|
-
const keybinds = categories.find((category) => category.id === KEYBINDS_CATEGORY_ID);
|
|
58
|
-
|
|
59
|
-
if (keybinds !== undefined) nodes.push({ id: keybinds.id, label: keybinds.name });
|
|
60
|
-
|
|
61
|
-
return nodes;
|
|
52
|
+
return categories.map((category) => ({ id: category.id, label: category.name }));
|
|
62
53
|
}
|
|
63
54
|
|
|
64
55
|
function KeybindsBody({
|
|
@@ -122,7 +113,7 @@ export function SettingsDialog({
|
|
|
122
113
|
<box style={{ flexDirection: "column", width: 20, paddingLeft: 1, paddingRight: 1 }}>
|
|
123
114
|
<Tree
|
|
124
115
|
nodes={navTree(categories)}
|
|
125
|
-
expandedIds={new Set(
|
|
116
|
+
expandedIds={new Set<string>()}
|
|
126
117
|
selectedId={category.id}
|
|
127
118
|
hideIcons
|
|
128
119
|
onSelect={onCategorySelect}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The sidebar thread tree: a Pinned / Projects / Threads listing. Selection stays
|
|
3
3
|
* with the keyboard grammar - the cursor indexes the flat thread order and this
|
|
4
4
|
* component renders the snapshot. A hovered or selected thread reveals a kebab
|
|
5
|
-
* that opens an inline pin / rename / delete menu; long
|
|
6
|
-
*
|
|
5
|
+
* that opens an inline pin / rename / delete menu; a long title clips to an
|
|
6
|
+
* ellipsis rather than wrapping. App supplies the surrounding chrome.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useState } from "react";
|
|
@@ -11,7 +11,7 @@ import type { Theme } from "../theme";
|
|
|
11
11
|
import { useComponentTheme } from "./theme-context";
|
|
12
12
|
import { IconButton } from "./primitives/IconButton";
|
|
13
13
|
import { NERD } from "./primitives/icons";
|
|
14
|
-
import {
|
|
14
|
+
import { truncateTitle } from "./truncate-title";
|
|
15
15
|
import { threadTitle, type InboxRow } from "./session-tree";
|
|
16
16
|
|
|
17
17
|
export interface ThreadTreeProps {
|
|
@@ -94,15 +94,7 @@ interface ThreadRowProps {
|
|
|
94
94
|
function ThreadRow(props: ThreadRowProps): React.ReactNode {
|
|
95
95
|
const { title, selected, pinned, titleWidth, menuOpen, onToggleMenu, tokens, theme } = props;
|
|
96
96
|
const [hovered, setHovered] = useState(false);
|
|
97
|
-
|
|
98
|
-
// leaving a colored band over the selected (elevated) or hovered (panel) row
|
|
99
|
-
const rowBackground = selected ? tokens.elevated : hovered ? tokens.panel : tokens.background;
|
|
100
|
-
const segments = fadeTitle(
|
|
101
|
-
title,
|
|
102
|
-
titleWidth,
|
|
103
|
-
selected ? tokens.accent : tokens.textMuted,
|
|
104
|
-
rowBackground,
|
|
105
|
-
);
|
|
97
|
+
const clippedTitle = truncateTitle(title, titleWidth);
|
|
106
98
|
const hasActions =
|
|
107
99
|
props.onPin !== undefined || props.onRename !== undefined || props.onDelete !== undefined;
|
|
108
100
|
|
|
@@ -116,13 +108,9 @@ function ThreadRow(props: ThreadRowProps): React.ReactNode {
|
|
|
116
108
|
backgroundColor: selected ? tokens.elevated : hovered ? tokens.panel : undefined,
|
|
117
109
|
}}
|
|
118
110
|
>
|
|
119
|
-
<text>
|
|
111
|
+
<text wrapMode="none">
|
|
120
112
|
<span fg={tokens.textDim}>{pinned ? ` ${NERD.pin} ` : " "}</span>
|
|
121
|
-
{
|
|
122
|
-
<span key={index} fg={segment.fg}>
|
|
123
|
-
{segment.text}
|
|
124
|
-
</span>
|
|
125
|
-
))}
|
|
113
|
+
<span fg={selected ? tokens.accent : tokens.textMuted}>{clippedTitle}</span>
|
|
126
114
|
</text>
|
|
127
115
|
<box style={{ flexGrow: 1 }} />
|
|
128
116
|
{(hovered || menuOpen) && hasActions ? (
|