@xp266/dshtui 0.1.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/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- package/src/vendor.ts +12 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import type { Ref } from 'react'
|
|
3
|
+
import { COLORS } from '../../theme.ts'
|
|
4
|
+
import { errorLine, loadingLine } from './status-lines.ts'
|
|
5
|
+
import type { SessionSummary } from '../../chat/session-list.ts'
|
|
6
|
+
import { groupSessions } from '../../chat/session-groups.ts'
|
|
7
|
+
import type { SessionSectionKind } from '../../chat/session-groups.ts'
|
|
8
|
+
import { useAsyncAction } from '../hooks/use-async-action.ts'
|
|
9
|
+
import { useAsyncList } from '../hooks/use-async-list.ts'
|
|
10
|
+
import { Dialog } from './dialog.tsx'
|
|
11
|
+
import { DIALOG_MAX_HEIGHT, DIALOG_WIDTH_LARGE } from './sizes.ts'
|
|
12
|
+
import type { DialogFooterLine, DialogHandle, DialogItem, DialogRow } from './dialog.tsx'
|
|
13
|
+
|
|
14
|
+
export interface SessionsApi {
|
|
15
|
+
listSessions(): Promise<SessionSummary[]>
|
|
16
|
+
openSession(id: string): Promise<void>
|
|
17
|
+
archiveSession(id: string): Promise<void>
|
|
18
|
+
activeSessionId(): string
|
|
19
|
+
newSession(): Promise<void>
|
|
20
|
+
onSessionsChanged?(listener: () => void): () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionsDialogProps {
|
|
24
|
+
api: SessionsApi
|
|
25
|
+
onClose: () => void
|
|
26
|
+
onBeforeSessionSelected?: () => void
|
|
27
|
+
onSessionSelected: (session: SessionSummary) => void
|
|
28
|
+
onNewSession?: () => void
|
|
29
|
+
ref?: Ref<DialogHandle>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ARM_TIMEOUT_MS = 3000
|
|
33
|
+
|
|
34
|
+
const SECTION_LABELS: Record<SessionSectionKind, string> = {
|
|
35
|
+
recent: 'Recent',
|
|
36
|
+
week: 'This Week',
|
|
37
|
+
older: 'Other',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ARCHIVE_HINT: DialogFooterLine[] = [
|
|
41
|
+
{ text: 'Ctrl+D to archive the session', color: COLORS.dialogHintText },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
interface ArmedRow {
|
|
45
|
+
key: string
|
|
46
|
+
id: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SessionsDialog({ api, onClose, onBeforeSessionSelected, onSessionSelected, onNewSession, ref }: SessionsDialogProps) {
|
|
50
|
+
const { error, clearError, run } = useAsyncAction()
|
|
51
|
+
const [armedKey, setArmedKey] = useState<string | null>(null)
|
|
52
|
+
const armedRef = useRef<string | null>(null)
|
|
53
|
+
const armTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
54
|
+
const itemRows = useRef(new Map<DialogItem, ArmedRow>())
|
|
55
|
+
const pendingArchives = useRef(new Set<string>())
|
|
56
|
+
const loadSessions = useCallback(async () => {
|
|
57
|
+
const rows = await api.listSessions()
|
|
58
|
+
if (pendingArchives.current.size === 0) return rows
|
|
59
|
+
const present = new Set(rows.map(row => row.id))
|
|
60
|
+
for (const id of [...pendingArchives.current]) {
|
|
61
|
+
if (!present.has(id)) pendingArchives.current.delete(id)
|
|
62
|
+
}
|
|
63
|
+
return rows.filter(row => !pendingArchives.current.has(row.id))
|
|
64
|
+
}, [api])
|
|
65
|
+
const selectSession = async (session: SessionSummary) => {
|
|
66
|
+
await run(async () => {
|
|
67
|
+
onBeforeSessionSelected?.()
|
|
68
|
+
await api.openSession(session.id)
|
|
69
|
+
onSessionSelected(session)
|
|
70
|
+
onClose()
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
const { items, loading, error: loadError, reload, remove } = useAsyncList(loadSessions)
|
|
74
|
+
const sections = useMemo(() => groupSessions(items), [items])
|
|
75
|
+
const disarm = () => {
|
|
76
|
+
clearTimeout(armTimer.current)
|
|
77
|
+
if (armedRef.current !== null) {
|
|
78
|
+
armedRef.current = null
|
|
79
|
+
setArmedKey(null)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
useEffect(() => () => clearTimeout(armTimer.current), [])
|
|
83
|
+
useEffect(() => api.onSessionsChanged?.(() => reload()), [api, reload])
|
|
84
|
+
const archive = (id: string) => {
|
|
85
|
+
const wasActive = api.activeSessionId() === id
|
|
86
|
+
pendingArchives.current.add(id)
|
|
87
|
+
remove(session => session.id === id)
|
|
88
|
+
disarm()
|
|
89
|
+
void api.archiveSession(id)
|
|
90
|
+
.then(() => {
|
|
91
|
+
if (wasActive) {
|
|
92
|
+
onBeforeSessionSelected?.()
|
|
93
|
+
onNewSession?.()
|
|
94
|
+
}
|
|
95
|
+
clearError()
|
|
96
|
+
})
|
|
97
|
+
.catch(cause => {
|
|
98
|
+
pendingArchives.current.delete(id)
|
|
99
|
+
void run(async () => {
|
|
100
|
+
throw cause instanceof Error ? cause : new Error(String(cause))
|
|
101
|
+
})
|
|
102
|
+
reload()
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
const handleCtrlD = (focused: DialogItem | undefined): boolean => {
|
|
106
|
+
if (focused?.type !== 'button') return false
|
|
107
|
+
const row = itemRows.current.get(focused)
|
|
108
|
+
if (row === undefined) return false
|
|
109
|
+
if (armedRef.current === row.key) {
|
|
110
|
+
disarm()
|
|
111
|
+
void archive(row.id)
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
clearTimeout(armTimer.current)
|
|
115
|
+
armedRef.current = row.key
|
|
116
|
+
setArmedKey(row.key)
|
|
117
|
+
armTimer.current = setTimeout(disarm, ARM_TIMEOUT_MS)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
const rows: DialogRow[] = []
|
|
121
|
+
const rowRefs = new Map<DialogItem, ArmedRow>()
|
|
122
|
+
sections.forEach((section, sectionIndex) => {
|
|
123
|
+
rows.push({ items: [{ type: 'header', label: SECTION_LABELS[section.kind], leadingBlank: sectionIndex > 0 }] })
|
|
124
|
+
for (const session of section.items) {
|
|
125
|
+
const key = `${section.kind}:${session.id}`
|
|
126
|
+
const armed = armedKey === key
|
|
127
|
+
const item: DialogItem = {
|
|
128
|
+
type: 'button',
|
|
129
|
+
label: session.name || session.id,
|
|
130
|
+
right: armed ? 'Press Ctrl+D again to archive' : session.ungrouped ? 'Ungrouped' : session.directory,
|
|
131
|
+
onPress: () => void selectSession(session),
|
|
132
|
+
...(armed ? { rightColor: COLORS.errorText } : {}),
|
|
133
|
+
}
|
|
134
|
+
rowRefs.set(item, { key, id: session.id })
|
|
135
|
+
rows.push({ items: [item] })
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
itemRows.current = rowRefs
|
|
140
|
+
})
|
|
141
|
+
const footer: DialogFooterLine[] = [...(loading ? [loadingLine()] : []), ...ARCHIVE_HINT]
|
|
142
|
+
const errors: DialogFooterLine[] = [
|
|
143
|
+
...(loadError !== null ? [errorLine(loadError)] : []),
|
|
144
|
+
...(error !== null ? [errorLine(error)] : []),
|
|
145
|
+
]
|
|
146
|
+
return (
|
|
147
|
+
<Dialog
|
|
148
|
+
ref={ref}
|
|
149
|
+
title="sessions"
|
|
150
|
+
width={DIALOG_WIDTH_LARGE}
|
|
151
|
+
maxHeight={DIALOG_MAX_HEIGHT}
|
|
152
|
+
rows={rows}
|
|
153
|
+
footer={footer}
|
|
154
|
+
errors={errors}
|
|
155
|
+
onClose={onClose}
|
|
156
|
+
search
|
|
157
|
+
searchRight
|
|
158
|
+
centerScroll
|
|
159
|
+
onCtrlD={handleCtrlD}
|
|
160
|
+
onActivity={disarm}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { COLORS } from '../../theme.ts'
|
|
2
|
+
import { glyphs } from '../../terminal/glyphs.ts'
|
|
3
|
+
import type { DialogFooterLine } from './geometry.ts'
|
|
4
|
+
|
|
5
|
+
export function loadingLine(): DialogFooterLine {
|
|
6
|
+
return { text: `loading${glyphs.ellipsis}`, color: COLORS.dialogHintText }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function errorLine(text: string): DialogFooterLine {
|
|
10
|
+
return { text, color: COLORS.errorText }
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Ref } from 'react'
|
|
2
|
+
import { Dialog } from './dialog.tsx'
|
|
3
|
+
import { DIALOG_WIDTH_MEDIUM, DIALOG_MAX_HEIGHT } from './sizes.ts'
|
|
4
|
+
import type { DialogHandle, DialogRow } from './dialog.tsx'
|
|
5
|
+
import { todoSymbol } from '../../chat/todo-view.ts'
|
|
6
|
+
import type { TodoItemLike } from '../../chat/todo-view.ts'
|
|
7
|
+
|
|
8
|
+
export interface TodoDialogProps {
|
|
9
|
+
ref?: Ref<DialogHandle>
|
|
10
|
+
api: readonly TodoItemLike[]
|
|
11
|
+
onClose: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function todoRows(todos: readonly TodoItemLike[]): DialogRow[] {
|
|
15
|
+
return todos.map(item => ({ items: [{ type: 'static', label: `${todoSymbol(item.status)} ${item.content}` }] }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TodoDialog({ ref, api, onClose }: TodoDialogProps) {
|
|
19
|
+
return (
|
|
20
|
+
<Dialog
|
|
21
|
+
ref={ref}
|
|
22
|
+
title="todo"
|
|
23
|
+
width={DIALOG_WIDTH_MEDIUM}
|
|
24
|
+
maxHeight={DIALOG_MAX_HEIGHT}
|
|
25
|
+
rows={todoRows(api)}
|
|
26
|
+
onClose={onClose}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useInput, usePaste } from 'ink'
|
|
2
|
+
import { isKeyConsumed } from '../key-arbiter.ts'
|
|
3
|
+
import type { RefObject } from 'react'
|
|
4
|
+
import { isMouseResidue } from '../../terminal/mouse.ts'
|
|
5
|
+
import { editBackspace, editDelete, editInsert } from '../../core/edit.ts'
|
|
6
|
+
import { adjustScroll, listCenterScroll } from './geometry.ts'
|
|
7
|
+
import { asTextItem, clampFocus, focusedItem, moveFocus } from './items.ts'
|
|
8
|
+
import type { DialogFocus, DialogItem, DialogRow } from './items.ts'
|
|
9
|
+
import { widgetOf } from '../widgets/registry.ts'
|
|
10
|
+
import '../widgets/index.ts'
|
|
11
|
+
import type { WidgetKeyApi } from '../widgets/types.ts'
|
|
12
|
+
|
|
13
|
+
export interface DialogLiveState {
|
|
14
|
+
rows: DialogRow[]
|
|
15
|
+
contentRows: DialogRow[]
|
|
16
|
+
focus: DialogFocus
|
|
17
|
+
scrollTop: number
|
|
18
|
+
cursor: number
|
|
19
|
+
value: string
|
|
20
|
+
searchValue: string
|
|
21
|
+
viewportHeight: number
|
|
22
|
+
focusMinRow: number
|
|
23
|
+
focusMaxRow: number
|
|
24
|
+
contentWidth: number
|
|
25
|
+
top: number
|
|
26
|
+
windowHeight: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NavigationSetters {
|
|
30
|
+
setFocus(focus: DialogFocus): void
|
|
31
|
+
setScrollTop(scrollTop: number): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyNavigation(live: DialogLiveState, direction: 'up' | 'down' | 'left' | 'right', search: boolean, setters: NavigationSetters, centerScroll = false): void {
|
|
35
|
+
const next = moveFocus(live.rows, live.focus, direction)
|
|
36
|
+
const clamped = clampFocus(live.rows, next, live.focusMinRow, live.focusMaxRow)
|
|
37
|
+
setters.setFocus(clamped)
|
|
38
|
+
const contentRow = Math.max(0, clamped.row - (search ? 1 : 0))
|
|
39
|
+
setters.setScrollTop(centerScroll && (direction === 'up' || direction === 'down')
|
|
40
|
+
? listCenterScroll(live.contentRows, contentRow, live.viewportHeight, live.contentWidth, direction)
|
|
41
|
+
: adjustScroll(live.contentRows, { row: contentRow, col: clamped.col }, live.scrollTop, live.viewportHeight, live.contentWidth))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function arrowFromRaw(input: string): 'up' | 'down' | 'left' | 'right' | null {
|
|
45
|
+
const match = /^(?:\[|O)(?:1;)?\d*(?::\d+)?([ABCD])$/.exec(input)
|
|
46
|
+
if (match === null) return null
|
|
47
|
+
const letter = match[1]!
|
|
48
|
+
return letter === 'A' ? 'up' : letter === 'B' ? 'down' : letter === 'C' ? 'right' : 'left'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DialogInputOptions {
|
|
52
|
+
live: RefObject<DialogLiveState>
|
|
53
|
+
search: boolean
|
|
54
|
+
closeGuarded: boolean
|
|
55
|
+
centerScroll?: boolean
|
|
56
|
+
onClose(): void
|
|
57
|
+
onCtrlA?(focused: DialogItem | undefined): boolean
|
|
58
|
+
onCtrlD?(focused: DialogItem | undefined): boolean
|
|
59
|
+
onCtrlE?(focused: DialogItem | undefined): boolean
|
|
60
|
+
onActivity?(): void
|
|
61
|
+
requestSearch(value: string): void
|
|
62
|
+
setFocus(focus: DialogFocus): void
|
|
63
|
+
setScrollTop(scrollTop: number): void
|
|
64
|
+
setCursor(cursor: number): void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useDialogInput(options: DialogInputOptions): void {
|
|
68
|
+
const { live, search, closeGuarded, onClose, onCtrlA, onCtrlD, onCtrlE, onActivity, requestSearch, setFocus, setScrollTop, setCursor } = options
|
|
69
|
+
const setters: NavigationSetters = { setFocus, setScrollTop }
|
|
70
|
+
const centerScroll = options.centerScroll === true
|
|
71
|
+
usePaste(text => {
|
|
72
|
+
onActivity?.()
|
|
73
|
+
const state = live.current
|
|
74
|
+
const current = focusedItem(state.rows[state.focus.row], state.focus.col)
|
|
75
|
+
const textItem = current === undefined ? null : asTextItem(current)
|
|
76
|
+
if (textItem === null) return
|
|
77
|
+
const normalized = text.replace(/\r\n?/g, '\n')
|
|
78
|
+
if (normalized === '') return
|
|
79
|
+
const next = editInsert({ value: textItem.value, cursor: state.cursor }, normalized)
|
|
80
|
+
textItem.onChange(next.value)
|
|
81
|
+
state.value = next.value
|
|
82
|
+
state.cursor = next.cursor
|
|
83
|
+
setCursor(next.cursor)
|
|
84
|
+
})
|
|
85
|
+
useInput((input, key) => {
|
|
86
|
+
if (isKeyConsumed()) return
|
|
87
|
+
const state = live.current
|
|
88
|
+
const liveCurrent = focusedItem(state.rows[state.focus.row], state.focus.col)
|
|
89
|
+
const rawArrow = arrowFromRaw(input)
|
|
90
|
+
const isUp = key.upArrow || rawArrow === 'up'
|
|
91
|
+
const isDown = key.downArrow || rawArrow === 'down'
|
|
92
|
+
const isLeft = key.leftArrow || rawArrow === 'left'
|
|
93
|
+
const isRight = key.rightArrow || rawArrow === 'right'
|
|
94
|
+
if (key.ctrl && input === 'a' && onCtrlA !== undefined && onCtrlA(liveCurrent)) return
|
|
95
|
+
if (key.ctrl && input === 'd' && onCtrlD !== undefined && onCtrlD(liveCurrent)) return
|
|
96
|
+
if (key.ctrl && input === 'e' && onCtrlE !== undefined && onCtrlE(liveCurrent)) return
|
|
97
|
+
onActivity?.()
|
|
98
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
99
|
+
if (!(key.ctrl && closeGuarded)) onClose()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const def = liveCurrent === undefined ? undefined : widgetOf(liveCurrent.type)
|
|
103
|
+
const keyApi: WidgetKeyApi = {
|
|
104
|
+
cursor: state.cursor,
|
|
105
|
+
subCol: state.focus.col,
|
|
106
|
+
setCursor(next) {
|
|
107
|
+
state.cursor = next
|
|
108
|
+
setCursor(next)
|
|
109
|
+
},
|
|
110
|
+
navigate(direction) {
|
|
111
|
+
applyNavigation(state, direction, search, setters, centerScroll)
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
if ((isLeft || isRight) && liveCurrent !== undefined && def !== undefined) {
|
|
115
|
+
if (def.onLeftRight?.(liveCurrent, isRight ? 1 : -1, keyApi) === true) return
|
|
116
|
+
}
|
|
117
|
+
if (isUp || isDown) {
|
|
118
|
+
applyNavigation(state, isUp ? 'up' : 'down', search, setters, centerScroll)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (key.return) {
|
|
122
|
+
if (liveCurrent === undefined || def === undefined) return
|
|
123
|
+
const handled = def.onEnter?.(liveCurrent, keyApi) ?? false
|
|
124
|
+
if (!handled) applyNavigation(state, 'down', search, setters, centerScroll)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (input === ' ' && liveCurrent !== undefined && def?.onSpace !== undefined) {
|
|
128
|
+
def.onSpace(liveCurrent)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const editingFormInput = liveCurrent !== undefined && liveCurrent.type !== 'search' && widgetOf(liveCurrent.type).editable === true
|
|
132
|
+
if (search && !editingFormInput) {
|
|
133
|
+
const v = state.searchValue
|
|
134
|
+
const onSearchRow = state.focus.row === 0
|
|
135
|
+
const caret = onSearchRow ? Math.max(0, Math.min(state.cursor, v.length)) : v.length
|
|
136
|
+
if (isLeft || isRight) {
|
|
137
|
+
const next = Math.max(0, Math.min(v.length, caret + (isRight ? 1 : -1)))
|
|
138
|
+
if (next !== caret) {
|
|
139
|
+
state.cursor = next
|
|
140
|
+
setCursor(next)
|
|
141
|
+
}
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const applyEdit = (next: { value: string; cursor: number } | null): boolean => {
|
|
145
|
+
if (next === null) return false
|
|
146
|
+
requestSearch(next.value)
|
|
147
|
+
state.cursor = next.cursor
|
|
148
|
+
setCursor(next.cursor)
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
if (key.backspace) {
|
|
152
|
+
applyEdit(editBackspace({ value: v, cursor: caret }))
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (key.delete) {
|
|
156
|
+
applyEdit(editDelete({ value: v, cursor: caret }))
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) {
|
|
160
|
+
applyEdit(editInsert({ value: v, cursor: caret }, input))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (def?.editable === true && liveCurrent !== undefined) {
|
|
165
|
+
const textItem = asTextItem(liveCurrent)
|
|
166
|
+
if (textItem === null) return
|
|
167
|
+
const apply = (next: { value: string; cursor: number } | null, trackCursor: boolean): boolean => {
|
|
168
|
+
if (next === null) return false
|
|
169
|
+
state.value = next.value
|
|
170
|
+
textItem.onChange(next.value)
|
|
171
|
+
if (trackCursor) {
|
|
172
|
+
state.cursor = next.cursor
|
|
173
|
+
setCursor(next.cursor)
|
|
174
|
+
}
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
if (key.backspace) {
|
|
178
|
+
apply(editBackspace({ value: state.value, cursor: state.cursor }), true)
|
|
179
|
+
} else if (key.delete) {
|
|
180
|
+
apply(editDelete({ value: state.value, cursor: state.cursor }), false)
|
|
181
|
+
} else if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) {
|
|
182
|
+
apply(editInsert({ value: state.value, cursor: state.cursor }, input), true)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { registerOverlay, registerStatusLine } from './contributions.ts'
|
|
3
|
+
import { registerWindow } from './windows.ts'
|
|
4
|
+
import { registerWindowService } from './window-services.ts'
|
|
5
|
+
import { registerWidget } from './widgets/registry.ts'
|
|
6
|
+
import { registerCommand } from './input/commands.ts'
|
|
7
|
+
import { registerKeyBinding } from './keymap.ts'
|
|
8
|
+
import { registerInputStatus } from './chrome/input-status.ts'
|
|
9
|
+
import { registerHomeLogo, subscribeHomeLogo } from './home-logo.ts'
|
|
10
|
+
import { registerHintArgsProvider, registerHintMatcher } from './chrome/hint-service.ts'
|
|
11
|
+
import { registerPalette, subscribePalettes } from '../theme.ts'
|
|
12
|
+
import { paletteColor } from '../theme.ts'
|
|
13
|
+
import { registerClipboardBackend } from '../terminal/clipboard-backends.ts'
|
|
14
|
+
import { registerToolView, subscribeToolViews } from '../chat/tool-views.ts'
|
|
15
|
+
import { registerChatNode, subscribeChatNodes } from '../chat/chat-nodes.ts'
|
|
16
|
+
import { registerMessageView, subscribeMessageViews } from './message/message-views.ts'
|
|
17
|
+
import { registerMessageRenderer } from './message/renderers.ts'
|
|
18
|
+
import { subscribeMessageRenderers } from './message/renderers.ts'
|
|
19
|
+
import { registerBootSink } from '../boot-log.ts'
|
|
20
|
+
import { registerFieldKind, specialFieldFactory } from '../core/fields.ts'
|
|
21
|
+
import { bumpSurface } from '../kernel/surface.ts'
|
|
22
|
+
import { registerMarkdownBlock, registerMarkdownInline, registerPrismGrammar } from '../ui/message/md/extensions.ts'
|
|
23
|
+
import { registerPointerHandler } from './pointer/registry.ts'
|
|
24
|
+
import { registerSelectionDomain, registerSelectionTransformer, registerSelectionClipboard } from './selection/service.ts'
|
|
25
|
+
import { registerPasteHandler } from '../ui/input/composer-paste.ts'
|
|
26
|
+
import { registerComposerKeyBinding } from '../ui/input/composer-keys.ts'
|
|
27
|
+
import { insertIntoComposer } from '../ui/input/composer-bus.ts'
|
|
28
|
+
import { createChatFace } from '../chat/chat-face.ts'
|
|
29
|
+
import type { ChatBridge } from '../chat/bridge.ts'
|
|
30
|
+
import type {
|
|
31
|
+
TuiChromeFace,
|
|
32
|
+
TuiExtensionPoint,
|
|
33
|
+
TuiInteractionsFace,
|
|
34
|
+
TuiServicesFace,
|
|
35
|
+
TuiWindowsFace,
|
|
36
|
+
} from '../contract/index.ts'
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
TuiChatFace,
|
|
40
|
+
TuiChromeFace,
|
|
41
|
+
TuiCommandsFace,
|
|
42
|
+
TuiContentFace,
|
|
43
|
+
TuiFieldsFace,
|
|
44
|
+
TuiHomeLogoFace,
|
|
45
|
+
TuiInteractionsFace,
|
|
46
|
+
TuiKeymapFace,
|
|
47
|
+
TuiPaletteFace,
|
|
48
|
+
TuiServicesFace,
|
|
49
|
+
TuiToolsFace,
|
|
50
|
+
TuiWidgetsFace,
|
|
51
|
+
TuiWindowsFace,
|
|
52
|
+
} from '../contract/index.ts'
|
|
53
|
+
|
|
54
|
+
export interface TuiExtensionPointHooks {
|
|
55
|
+
/** Invoked after a palette/content contribution changes the rendered surface. */
|
|
56
|
+
onContributionsChanged?(): void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Publish the TUI extension point as the `tui` cordis service so other
|
|
61
|
+
* plugins can contribute windows, window services, chrome, widgets,
|
|
62
|
+
* palettes, key bindings, commands, tool views, chat nodes, message views,
|
|
63
|
+
* interaction panels, and the bridge-backed chat face without importing
|
|
64
|
+
* this package's modules (which would fork the registries). Registration
|
|
65
|
+
* lifetimes ride the contributing plugin's own fiber: callers receive
|
|
66
|
+
* disposers and the registries only hold live entries.
|
|
67
|
+
*/
|
|
68
|
+
export function createTuiExtensionPoint(ctx: Context, hooks: TuiExtensionPointHooks = {}): { extension: TuiExtensionPoint; dispose(): void } {
|
|
69
|
+
const extension: TuiExtensionPoint = {
|
|
70
|
+
windows: { register: registerWindow },
|
|
71
|
+
services: { register: registerWindowService },
|
|
72
|
+
chrome: {
|
|
73
|
+
statusLine: { register: registerStatusLine },
|
|
74
|
+
overlays: { register: registerOverlay },
|
|
75
|
+
widgets: { register: registerWidget },
|
|
76
|
+
palette: { register: registerPalette, color: paletteColor },
|
|
77
|
+
keys: { register: registerKeyBinding },
|
|
78
|
+
inputStatus: { register: registerInputStatus },
|
|
79
|
+
logo: { register: registerHomeLogo },
|
|
80
|
+
},
|
|
81
|
+
commands: { register: registerCommand },
|
|
82
|
+
tools: { register: registerToolView },
|
|
83
|
+
content: {
|
|
84
|
+
nodes: { register: registerChatNode },
|
|
85
|
+
views: { register: registerMessageView },
|
|
86
|
+
renderers: { register: registerMessageRenderer },
|
|
87
|
+
},
|
|
88
|
+
startup: { registerSink: registerBootSink },
|
|
89
|
+
fields: {
|
|
90
|
+
register(contribution) {
|
|
91
|
+
const off = registerFieldKind(contribution)
|
|
92
|
+
surfaceChanged()
|
|
93
|
+
return () => {
|
|
94
|
+
off()
|
|
95
|
+
surfaceChanged()
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
factory: specialFieldFactory(),
|
|
99
|
+
},
|
|
100
|
+
composer: {
|
|
101
|
+
paste: { register: registerPasteHandler },
|
|
102
|
+
keys: { register: registerComposerKeyBinding },
|
|
103
|
+
insert: insertIntoComposer,
|
|
104
|
+
},
|
|
105
|
+
hint: {
|
|
106
|
+
matchers: { register: registerHintMatcher },
|
|
107
|
+
args: { register: registerHintArgsProvider },
|
|
108
|
+
},
|
|
109
|
+
clipboard: { register: registerClipboardBackend },
|
|
110
|
+
pointer: { register: registerPointerHandler },
|
|
111
|
+
selection: {
|
|
112
|
+
domains: { register: registerSelectionDomain },
|
|
113
|
+
transformers: { register: registerSelectionTransformer },
|
|
114
|
+
clipboard: { register: registerSelectionClipboard },
|
|
115
|
+
},
|
|
116
|
+
markdown: {
|
|
117
|
+
blocks: {
|
|
118
|
+
register(contribution) {
|
|
119
|
+
const off = registerMarkdownBlock(contribution)
|
|
120
|
+
contentChanged()
|
|
121
|
+
return () => {
|
|
122
|
+
off()
|
|
123
|
+
contentChanged()
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
inline: {
|
|
128
|
+
register(contribution) {
|
|
129
|
+
const off = registerMarkdownInline(contribution)
|
|
130
|
+
contentChanged()
|
|
131
|
+
return () => {
|
|
132
|
+
off()
|
|
133
|
+
contentChanged()
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
languages: {
|
|
138
|
+
register(contribution) {
|
|
139
|
+
const off = registerPrismGrammar(contribution)
|
|
140
|
+
contentChanged()
|
|
141
|
+
return () => {
|
|
142
|
+
off()
|
|
143
|
+
contentChanged()
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
const surfaceChanged = (): void => {
|
|
150
|
+
bumpSurface()
|
|
151
|
+
hooks.onContributionsChanged?.()
|
|
152
|
+
}
|
|
153
|
+
const offToolViews = subscribeToolViews(surfaceChanged)
|
|
154
|
+
const offMessageViews = subscribeMessageViews(surfaceChanged)
|
|
155
|
+
const offPalettes = subscribePalettes(surfaceChanged)
|
|
156
|
+
const offMessageRenderers = subscribeMessageRenderers(surfaceChanged)
|
|
157
|
+
const offChatNodes = subscribeChatNodes(surfaceChanged)
|
|
158
|
+
const offHomeLogo = subscribeHomeLogo(surfaceChanged)
|
|
159
|
+
const contentChanged = (): void => {
|
|
160
|
+
surfaceChanged()
|
|
161
|
+
}
|
|
162
|
+
const disposeService = ctx.provide('tui', extension)
|
|
163
|
+
return {
|
|
164
|
+
extension,
|
|
165
|
+
dispose() {
|
|
166
|
+
offToolViews()
|
|
167
|
+
offMessageViews()
|
|
168
|
+
offPalettes()
|
|
169
|
+
offMessageRenderers()
|
|
170
|
+
offChatNodes()
|
|
171
|
+
offHomeLogo()
|
|
172
|
+
disposeService()
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Attach the bridge-backed faces once the chat bridge exists; returns the disposer. */
|
|
178
|
+
export function exposeRuntimeFaces(extension: TuiExtensionPoint, bridge: ChatBridge): () => void {
|
|
179
|
+
extension.interactions = {
|
|
180
|
+
panels: bridge.interactions.panels,
|
|
181
|
+
push: (kind, request, signal) => bridge.interactions.pushRequest(kind, request, signal),
|
|
182
|
+
}
|
|
183
|
+
extension.chat = createChatFace(bridge)
|
|
184
|
+
return () => {
|
|
185
|
+
extension.interactions = undefined
|
|
186
|
+
extension.chat = undefined
|
|
187
|
+
}
|
|
188
|
+
}
|