opencode-sesh 0.12.0 → 0.13.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/CHANGELOG.md +11 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +265 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Right-clicking a session in the TUI sidebar opens a floating menu at the click
|
|
15
|
+
with Open, Preview transcript, Open in new cmux workspace (inside cmux), Fork,
|
|
16
|
+
Pin/Unpin and Delete (with confirmation). It stays inside the terminal, moves
|
|
17
|
+
with ↑/↓/Enter, and closes on Esc or a click elsewhere. The home list and
|
|
18
|
+
picker keep the direct cmux action.
|
|
10
19
|
## [0.12.0] - 2026-09-30
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -388,7 +397,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
388
397
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
389
398
|
CI.
|
|
390
399
|
|
|
391
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.
|
|
400
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.13.0...HEAD
|
|
401
|
+
[0.13.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.12.0...v0.13.0
|
|
392
402
|
[0.12.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.1...v0.12.0
|
|
393
403
|
[0.11.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.11.0...v0.11.1
|
|
394
404
|
[0.11.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.10.0...v0.11.0
|
package/README.md
CHANGED
|
@@ -185,7 +185,11 @@ The picker groups sessions under collapsible directory headings (click ▾/▸).
|
|
|
185
185
|
Mouse-wheel scrolling moves through the tree without changing the selection
|
|
186
186
|
under a stationary pointer. Option-P opens a full-size transcript preview over
|
|
187
187
|
the picker; use ↑/↓ to preview other sessions, then Esc to return to the list.
|
|
188
|
-
Right-click a
|
|
188
|
+
Right-click a sidebar session to open its action menu: open, preview, fork,
|
|
189
|
+
pin/unpin, or delete with confirmation. Inside cmux, the menu also offers opening
|
|
190
|
+
the session in a new workspace at its directory. Use ↑/↓ and Enter to choose an
|
|
191
|
+
action, or Esc to dismiss the menu.
|
|
192
|
+
Right-click a session in the home list or picker to open it in a new cmux
|
|
189
193
|
workspace at its directory; outside cmux it shows a toast instead.
|
|
190
194
|
|
|
191
195
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
package/package.json
CHANGED
package/tui/sesh-panel.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import type { Project, Session } from "@opencode-ai/sdk/v2"
|
|
4
|
-
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
|
4
|
+
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from "solid-js"
|
|
5
5
|
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises"
|
|
6
6
|
import { dirname, join } from "node:path"
|
|
7
7
|
import { spawn } from "node:child_process"
|
|
8
8
|
import { getTreeSitterClient, RGBA, SyntaxStyle, TextAttributes } from "@opentui/core"
|
|
9
|
+
import { Portal } from "@opentui/solid"
|
|
9
10
|
|
|
10
11
|
type ThemeColors = TuiPluginApi["theme"]["current"]
|
|
11
12
|
|
|
@@ -251,6 +252,7 @@ type Entry = { id: string; title: string; dir: string; group: string; updated: n
|
|
|
251
252
|
type EntryResult = { entries: Entry[]; truncated: boolean }
|
|
252
253
|
type SidebarMarker = "current" | "running" | "idle"
|
|
253
254
|
|
|
255
|
+
const LEFT_BUTTON = 0
|
|
254
256
|
const RIGHT_BUTTON = 2
|
|
255
257
|
|
|
256
258
|
function openInCmuxWorkspace(api: TuiPluginApi, entry: Entry) {
|
|
@@ -267,6 +269,128 @@ function openInCmuxWorkspace(api: TuiPluginApi, entry: Entry) {
|
|
|
267
269
|
})
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
type MenuAction = "open" | "preview" | "workspace" | "fork" | "pin" | "delete" | "confirm" | "cancel"
|
|
273
|
+
type MenuItem = { id: MenuAction; label: string }
|
|
274
|
+
type MenuState = { entry: Entry; x: number; y: number; items: MenuItem[]; index: number; confirming: boolean }
|
|
275
|
+
type MenuLayer = {
|
|
276
|
+
mode: "base"
|
|
277
|
+
priority: number
|
|
278
|
+
bindings: { key: string; desc: string; preventDefault: boolean; cmd: () => void }[]
|
|
279
|
+
}
|
|
280
|
+
type ContextMenuHost = {
|
|
281
|
+
registerLayer: (layer: MenuLayer) => () => void
|
|
282
|
+
actions: Record<"open" | "preview" | "workspace" | "fork" | "pin" | "delete", (entry: Entry) => void>
|
|
283
|
+
pinned: (entry: Entry) => boolean
|
|
284
|
+
cmux: () => boolean
|
|
285
|
+
blocked: () => boolean
|
|
286
|
+
onChange: (state: MenuState | undefined) => void
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const CONTEXT_MENU_LAYER_PRIORITY = 30
|
|
290
|
+
|
|
291
|
+
function contextMenuItems(pinned: boolean, cmux: boolean): MenuItem[] {
|
|
292
|
+
return [
|
|
293
|
+
{ id: "open", label: "Open" },
|
|
294
|
+
{ id: "preview", label: "Preview transcript" },
|
|
295
|
+
...(cmux ? [{ id: "workspace" as const, label: "Open in new cmux workspace" }] : []),
|
|
296
|
+
{ id: "fork", label: "Fork" },
|
|
297
|
+
{ id: "pin", label: pinned ? "Unpin" : "Pin" },
|
|
298
|
+
{ id: "delete", label: "Delete" },
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function contextMenuSize(items: MenuItem[]): { width: number; height: number } {
|
|
303
|
+
return { width: Math.max(...items.map((item) => item.label.length)) + 4, height: items.length + 2 }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function contextMenuBox(
|
|
307
|
+
anchor: { x: number; y: number; width: number; height: number },
|
|
308
|
+
cols: number,
|
|
309
|
+
rows: number,
|
|
310
|
+
): { left: number; top: number; width: number; height: number } {
|
|
311
|
+
const width = Math.max(1, Math.min(anchor.width, cols))
|
|
312
|
+
const height = Math.max(1, Math.min(anchor.height, rows))
|
|
313
|
+
const left = Math.max(0, Math.min(anchor.x, cols - width))
|
|
314
|
+
const below = anchor.y + 1
|
|
315
|
+
const top = below + height <= rows ? below : Math.max(0, anchor.y - height)
|
|
316
|
+
return { left, top, width, height }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createContextMenu(host: ContextMenuHost) {
|
|
320
|
+
let state: MenuState | undefined
|
|
321
|
+
let dispose: (() => void) | undefined
|
|
322
|
+
|
|
323
|
+
const publish = (next: MenuState | undefined) => {
|
|
324
|
+
state = next
|
|
325
|
+
host.onChange(next)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const close = () => {
|
|
329
|
+
dispose?.()
|
|
330
|
+
dispose = undefined
|
|
331
|
+
if (state) publish(undefined)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const select = (index: number) => {
|
|
335
|
+
if (state) publish({ ...state, index: Math.max(0, Math.min(state.items.length - 1, index)) })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const move = (delta: number) => {
|
|
339
|
+
if (!state) return
|
|
340
|
+
const count = state.items.length
|
|
341
|
+
publish({ ...state, index: (state.index + delta + count) % count })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const activate = (index?: number) => {
|
|
345
|
+
if (!state) return
|
|
346
|
+
const current = state
|
|
347
|
+
const item = current.items[index ?? current.index]
|
|
348
|
+
if (!item) return
|
|
349
|
+
if (item.id === "delete") {
|
|
350
|
+
publish({
|
|
351
|
+
...current,
|
|
352
|
+
confirming: true,
|
|
353
|
+
items: [
|
|
354
|
+
{ id: "confirm", label: "Confirm delete" },
|
|
355
|
+
{ id: "cancel", label: "Cancel" },
|
|
356
|
+
],
|
|
357
|
+
index: 1,
|
|
358
|
+
})
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
close()
|
|
362
|
+
if (item.id === "confirm") host.actions.delete(current.entry)
|
|
363
|
+
else if (item.id !== "cancel") host.actions[item.id](current.entry)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const click = (index: number, button?: number) => {
|
|
367
|
+
if ((button ?? LEFT_BUTTON) === LEFT_BUTTON) activate(index)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const guarded = (run: () => void) => () => {
|
|
371
|
+
if (host.blocked()) close()
|
|
372
|
+
else run()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const open = (entry: Entry, x: number, y: number) => {
|
|
376
|
+
if (host.blocked()) return
|
|
377
|
+
publish({ entry, x, y, items: contextMenuItems(host.pinned(entry), host.cmux()), index: 0, confirming: false })
|
|
378
|
+
if (dispose) return
|
|
379
|
+
dispose = host.registerLayer({
|
|
380
|
+
mode: "base",
|
|
381
|
+
priority: CONTEXT_MENU_LAYER_PRIORITY,
|
|
382
|
+
bindings: [
|
|
383
|
+
{ key: "up", desc: "Previous menu item", preventDefault: true, cmd: guarded(() => move(-1)) },
|
|
384
|
+
{ key: "down", desc: "Next menu item", preventDefault: true, cmd: guarded(() => move(1)) },
|
|
385
|
+
{ key: "enter", desc: "Choose menu item", preventDefault: true, cmd: guarded(() => activate()) },
|
|
386
|
+
{ key: "escape", desc: "Close menu", preventDefault: true, cmd: close },
|
|
387
|
+
],
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { open, close, select, activate, click, current: () => state }
|
|
392
|
+
}
|
|
393
|
+
|
|
270
394
|
// Loading-spinner frames for the marker of a session whose agent is working.
|
|
271
395
|
const SIDEBAR_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
272
396
|
|
|
@@ -784,6 +908,14 @@ function pickerLastStart(rows: PickerRow[], height: number, excerpt: (entry: Ent
|
|
|
784
908
|
return start
|
|
785
909
|
}
|
|
786
910
|
|
|
911
|
+
type PortalContainer = {
|
|
912
|
+
position?: string
|
|
913
|
+
left?: number
|
|
914
|
+
top?: number
|
|
915
|
+
zIndex?: number
|
|
916
|
+
destroyRecursively?: () => void
|
|
917
|
+
}
|
|
918
|
+
|
|
787
919
|
type PinProps = { pins: () => Pins; onTogglePin: (kind: keyof Pins, value: string) => void; refreshPins: () => void }
|
|
788
920
|
|
|
789
921
|
function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
@@ -802,6 +934,8 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
802
934
|
const [cursor, setCursor] = createSignal(0)
|
|
803
935
|
const [pendingDelete, setPendingDelete] = createSignal<string>()
|
|
804
936
|
const [waiting, setWaiting] = createSignal<Map<string, string>>(new Map())
|
|
937
|
+
const [menuState, setMenuState] = createSignal<MenuState>()
|
|
938
|
+
let portalContainer: PortalContainer | undefined
|
|
805
939
|
let confirmTimer: ReturnType<typeof setTimeout> | undefined
|
|
806
940
|
const currentID = createMemo(() => {
|
|
807
941
|
const route = props.api.route.current
|
|
@@ -907,6 +1041,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
907
1041
|
}
|
|
908
1042
|
|
|
909
1043
|
const toggleSection = () => {
|
|
1044
|
+
menu.close()
|
|
910
1045
|
if (!sectionCollapsed()) {
|
|
911
1046
|
cancelDelete()
|
|
912
1047
|
setNavActive(false)
|
|
@@ -918,6 +1053,47 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
918
1053
|
|
|
919
1054
|
const openSession = (entry: Entry) => props.api.route.navigate("session", { sessionID: entry.id })
|
|
920
1055
|
|
|
1056
|
+
const forkEntry = async (entry: Entry) => {
|
|
1057
|
+
try {
|
|
1058
|
+
const result = await props.api.client.session.fork({
|
|
1059
|
+
sessionID: entry.id,
|
|
1060
|
+
directory: entry.dir || undefined,
|
|
1061
|
+
})
|
|
1062
|
+
const created = (result?.data ?? result) as Session | undefined
|
|
1063
|
+
if (!created?.id) throw new Error("fork returned no session")
|
|
1064
|
+
props.api.route.navigate("session", { sessionID: created.id })
|
|
1065
|
+
props.api.ui.toast({ message: `Forked "${truncate(entry.title, 40)}"`, variant: "info" })
|
|
1066
|
+
} catch {
|
|
1067
|
+
props.api.ui.toast({ message: "Could not fork session", variant: "error" })
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const menu = createContextMenu({
|
|
1072
|
+
registerLayer: (layer) => props.api.keymap.registerLayer(layer),
|
|
1073
|
+
actions: {
|
|
1074
|
+
open: (entry) => void openSession(entry),
|
|
1075
|
+
preview: (entry) => preview.open(entry),
|
|
1076
|
+
workspace: (entry) => openInCmuxWorkspace(props.api, entry),
|
|
1077
|
+
fork: (entry) => void forkEntry(entry),
|
|
1078
|
+
pin: (entry) => props.onTogglePin("sessions", entry.id),
|
|
1079
|
+
delete: (entry) => void deleteEntry(entry),
|
|
1080
|
+
},
|
|
1081
|
+
pinned: (entry) => props.pins().sessions.includes(entry.id),
|
|
1082
|
+
cmux: () => Boolean(process.env.CMUX_WORKSPACE_ID),
|
|
1083
|
+
blocked: () => props.api.ui.dialog.open,
|
|
1084
|
+
onChange: setMenuState,
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
const menuBox = createMemo(() => {
|
|
1088
|
+
const state = menuState()
|
|
1089
|
+
if (!state) return undefined
|
|
1090
|
+
return contextMenuBox(
|
|
1091
|
+
{ x: state.x, y: state.y, ...contextMenuSize(state.items) },
|
|
1092
|
+
props.api.renderer.width,
|
|
1093
|
+
props.api.renderer.height,
|
|
1094
|
+
)
|
|
1095
|
+
})
|
|
1096
|
+
|
|
921
1097
|
// Flat order of the rows a cursor can land on, so keyboard navigation and
|
|
922
1098
|
// mouse hover resolve to the same row.
|
|
923
1099
|
const itemRows = createMemo(() => tree().flatMap((row) => (row.kind === "item" ? [row.entry] : [])))
|
|
@@ -938,6 +1114,20 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
938
1114
|
previousRows = rows
|
|
939
1115
|
})
|
|
940
1116
|
|
|
1117
|
+
const routeKey = createMemo(() => {
|
|
1118
|
+
const route = props.api.route.current
|
|
1119
|
+
const id = route.name === "session" && typeof route.params?.sessionID === "string" ? route.params.sessionID : ""
|
|
1120
|
+
return `${route.name}:${id}`
|
|
1121
|
+
})
|
|
1122
|
+
createEffect(on(routeKey, () => menu.close(), { defer: true }))
|
|
1123
|
+
createEffect(() => {
|
|
1124
|
+
const state = menuState()
|
|
1125
|
+
if (state && !itemRows().some((entry) => entry.id === state.entry.id)) menu.close()
|
|
1126
|
+
})
|
|
1127
|
+
createEffect(() => {
|
|
1128
|
+
if (props.api.ui.dialog.open) menu.close()
|
|
1129
|
+
})
|
|
1130
|
+
|
|
941
1131
|
// Marker only: a spinner for a working agent, the open-session dot, and the
|
|
942
1132
|
// neutral row for idle sessions. Titles stay plain and no status text is
|
|
943
1133
|
// rendered.
|
|
@@ -1072,6 +1262,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1072
1262
|
})
|
|
1073
1263
|
|
|
1074
1264
|
const onRootMouseDown = () => {
|
|
1265
|
+
menu.close()
|
|
1075
1266
|
if (insideSearchBox) {
|
|
1076
1267
|
insideSearchBox = false
|
|
1077
1268
|
return
|
|
@@ -1082,7 +1273,12 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1082
1273
|
onMount(() => {
|
|
1083
1274
|
ensureRootMouse(props.api.renderer)
|
|
1084
1275
|
rootMouseHandlers.add(onRootMouseDown)
|
|
1085
|
-
|
|
1276
|
+
const closeMenu = () => menu.close()
|
|
1277
|
+
props.api.renderer.on("resize", closeMenu)
|
|
1278
|
+
onCleanup(() => {
|
|
1279
|
+
rootMouseHandlers.delete(onRootMouseDown)
|
|
1280
|
+
props.api.renderer.off("resize", closeMenu)
|
|
1281
|
+
})
|
|
1086
1282
|
})
|
|
1087
1283
|
|
|
1088
1284
|
const appendQuery = (input: string) => {
|
|
@@ -1236,6 +1432,10 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1236
1432
|
disposeSearch?.()
|
|
1237
1433
|
disposeNav?.()
|
|
1238
1434
|
disposeConfirm?.()
|
|
1435
|
+
menu.close()
|
|
1436
|
+
const container = portalContainer
|
|
1437
|
+
portalContainer = undefined
|
|
1438
|
+
process.nextTick(() => container?.destroyRecursively?.())
|
|
1239
1439
|
if (confirmTimer) clearTimeout(confirmTimer)
|
|
1240
1440
|
})
|
|
1241
1441
|
|
|
@@ -1299,6 +1499,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1299
1499
|
flexShrink={1}
|
|
1300
1500
|
verticalScrollbarOptions={{ visible: false }}
|
|
1301
1501
|
horizontalScrollbarOptions={{ visible: false }}
|
|
1502
|
+
onMouseScroll={() => menu.close()}
|
|
1302
1503
|
>
|
|
1303
1504
|
<For each={tree()}>
|
|
1304
1505
|
{(row) => {
|
|
@@ -1336,9 +1537,12 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1336
1537
|
}
|
|
1337
1538
|
}}
|
|
1338
1539
|
onMouseOut={() => setHovered(undefined)}
|
|
1339
|
-
onMouseDown={(event: { button?: number }) =>
|
|
1340
|
-
event.button === RIGHT_BUTTON
|
|
1341
|
-
|
|
1540
|
+
onMouseDown={(event: { button?: number; x: number; y: number; stopPropagation: () => void }) => {
|
|
1541
|
+
if (event.button === RIGHT_BUTTON) {
|
|
1542
|
+
event.stopPropagation()
|
|
1543
|
+
menu.open(row.entry, event.x, event.y)
|
|
1544
|
+
} else void openSession(row.entry)
|
|
1545
|
+
}}
|
|
1342
1546
|
>
|
|
1343
1547
|
<text flexShrink={0}>
|
|
1344
1548
|
<span style={{ fg: theme().textMuted }}>{row.last ? "└" : "├"}</span>
|
|
@@ -1383,6 +1587,62 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1383
1587
|
</Show>
|
|
1384
1588
|
</Show>
|
|
1385
1589
|
</Show>
|
|
1590
|
+
<Portal
|
|
1591
|
+
mount={props.api.renderer.root as never}
|
|
1592
|
+
ref={(container: PortalContainer) => {
|
|
1593
|
+
portalContainer = container
|
|
1594
|
+
container.position = "absolute"
|
|
1595
|
+
container.left = 0
|
|
1596
|
+
container.top = 0
|
|
1597
|
+
container.zIndex = 1000
|
|
1598
|
+
}}
|
|
1599
|
+
>
|
|
1600
|
+
<Show when={menuState()}>
|
|
1601
|
+
<box
|
|
1602
|
+
position="absolute"
|
|
1603
|
+
left={menuBox()!.left}
|
|
1604
|
+
top={menuBox()!.top}
|
|
1605
|
+
width={menuBox()!.width}
|
|
1606
|
+
height={menuBox()!.height}
|
|
1607
|
+
flexDirection="column"
|
|
1608
|
+
border
|
|
1609
|
+
borderStyle="rounded"
|
|
1610
|
+
borderColor={theme().borderActive}
|
|
1611
|
+
backgroundColor={theme().backgroundPanel}
|
|
1612
|
+
paddingLeft={1}
|
|
1613
|
+
paddingRight={1}
|
|
1614
|
+
onMouseDown={(event: { stopPropagation: () => void }) => event.stopPropagation()}
|
|
1615
|
+
>
|
|
1616
|
+
<For each={menuState()!.items}>
|
|
1617
|
+
{(item, index) => (
|
|
1618
|
+
<box
|
|
1619
|
+
height={1}
|
|
1620
|
+
backgroundColor={menuState()!.index === index() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
|
|
1621
|
+
onMouseOver={() => menu.select(index())}
|
|
1622
|
+
onMouseDown={(event: { button?: number; stopPropagation: () => void }) => {
|
|
1623
|
+
event.stopPropagation()
|
|
1624
|
+
menu.click(index(), event.button)
|
|
1625
|
+
}}
|
|
1626
|
+
>
|
|
1627
|
+
<text
|
|
1628
|
+
wrapMode="none"
|
|
1629
|
+
style={{
|
|
1630
|
+
fg:
|
|
1631
|
+
item.id === "delete" || item.id === "confirm"
|
|
1632
|
+
? theme().error
|
|
1633
|
+
: menuState()!.index === index()
|
|
1634
|
+
? theme().text
|
|
1635
|
+
: theme().textMuted,
|
|
1636
|
+
}}
|
|
1637
|
+
>
|
|
1638
|
+
{item.label}
|
|
1639
|
+
</text>
|
|
1640
|
+
</box>
|
|
1641
|
+
)}
|
|
1642
|
+
</For>
|
|
1643
|
+
</box>
|
|
1644
|
+
</Show>
|
|
1645
|
+
</Portal>
|
|
1386
1646
|
</box>
|
|
1387
1647
|
)
|
|
1388
1648
|
}
|