@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.
Files changed (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. 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,7 @@
1
+ export const DIALOG_WIDTH_MEDIUM = 60
2
+ export const DIALOG_WIDTH_LARGE = 88
3
+
4
+ export const DIALOG_MAX_HEIGHT = 0.6
5
+
6
+ export const INPUT_MAX_ROWS = 3
7
+ export const ERROR_MAX_ROWS = 3
@@ -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
+ }