@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,53 @@
|
|
|
1
|
+
import { registerToolView } from './tool-views.ts'
|
|
2
|
+
import { ASK_USER_TOOL_NAME, formatAskAnswered, formatAskQuestions, parseAskAnswers, parseAskQuestions } from './question-view.ts'
|
|
3
|
+
import { TODO_TOOL_NAME, formatTodoBubble, parseTodoArgs } from './todo-view.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The interactive builtins ride the same contribution registry third-party
|
|
7
|
+
* plugins use: keyed by tool name, `takeover` keeps the protocol presenter
|
|
8
|
+
* out. The call renders the pending interaction; the ask result rewrites the
|
|
9
|
+
* card into question-plus-choice so the settled record shows what was picked,
|
|
10
|
+
* not the offer again. The todo checklist already IS the settled state (the
|
|
11
|
+
* call args carry the new list), so its result stays a wipe.
|
|
12
|
+
*/
|
|
13
|
+
let registered: (() => void) | undefined
|
|
14
|
+
|
|
15
|
+
export function registerBuiltinToolViews(): () => void {
|
|
16
|
+
if (registered !== undefined) return registered
|
|
17
|
+
const offTodo = registerToolView({
|
|
18
|
+
tool: TODO_TOOL_NAME,
|
|
19
|
+
takeover: true,
|
|
20
|
+
call: ({ tool, args }) => {
|
|
21
|
+
const items = parseTodoArgs(args)
|
|
22
|
+
return { label: tool, body: items.length === 0 ? '' : formatTodoBubble(items) }
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
const offAsk = registerToolView({
|
|
26
|
+
tool: ASK_USER_TOOL_NAME,
|
|
27
|
+
takeover: true,
|
|
28
|
+
call: ({ tool, args }) => ({ label: tool, body: formatAskQuestions(args) }),
|
|
29
|
+
result: ({ result, args }) => {
|
|
30
|
+
const answers = parseAskAnswers(resultTextOf(result))
|
|
31
|
+
if (answers.length === 0) return { kind: 'replace', text: '' }
|
|
32
|
+
return {
|
|
33
|
+
kind: 'replace',
|
|
34
|
+
text: formatAskAnswered(parseAskQuestions(args), answers),
|
|
35
|
+
argsBody: '',
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
registered = () => {
|
|
40
|
+
offTodo()
|
|
41
|
+
offAsk()
|
|
42
|
+
registered = undefined
|
|
43
|
+
}
|
|
44
|
+
return registered
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resultTextOf(result: { content: readonly { type: string; text?: string }[] }): string {
|
|
48
|
+
let text = ''
|
|
49
|
+
for (const block of result.content) {
|
|
50
|
+
if (block.type === 'text') text += block.text ?? ''
|
|
51
|
+
}
|
|
52
|
+
return text
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PendingImage } from '../core/paste.ts'
|
|
2
|
+
import type { TuiChatFace } from '../contract/index.ts'
|
|
3
|
+
import type { ChatBridge } from './bridge.ts'
|
|
4
|
+
|
|
5
|
+
export function createChatFace(bridge: ChatBridge): TuiChatFace {
|
|
6
|
+
return {
|
|
7
|
+
cwd: () => bridge.cwd(),
|
|
8
|
+
activeSessionId: () => bridge.activeSessionId(),
|
|
9
|
+
onEvent: listener => bridge.subscribe(listener),
|
|
10
|
+
send: (text, images) => bridge.send(text, images),
|
|
11
|
+
interrupt: () => bridge.interrupt(),
|
|
12
|
+
newSession: () => bridge.newSession(),
|
|
13
|
+
openSession: id => bridge.openSession(id),
|
|
14
|
+
listSessions: () => bridge.listSessions(),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { keyedRegistry } from '../kernel/registry.ts'
|
|
2
|
+
import type { ChatNodeDefinition, CustomMessage } from '../contract/index.ts'
|
|
3
|
+
import type { Message } from '../model/message.ts'
|
|
4
|
+
import type { TurnState } from './store.ts'
|
|
5
|
+
|
|
6
|
+
const definitions = keyedRegistry<ChatNodeDefinition>()
|
|
7
|
+
|
|
8
|
+
export function registerChatNode(definition: ChatNodeDefinition): () => void {
|
|
9
|
+
return definitions.register(definition.id, definition, { order: definition.order })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function subscribeChatNodes(listener: () => void): () => void {
|
|
13
|
+
return definitions.subscribe(listener)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let sequence = 0
|
|
17
|
+
|
|
18
|
+
function nextNodeId(nodeId: string): string {
|
|
19
|
+
sequence += 1
|
|
20
|
+
return `node-${nodeId}-${sequence}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function claimChatNode(messages: Message[], event: Parameters<ChatNodeDefinition['match']>[0], turn: TurnState, definition: ChatNodeDefinition): boolean {
|
|
24
|
+
const claimedId = turn.chatNodes.get(definition.id)
|
|
25
|
+
if (claimedId !== undefined) {
|
|
26
|
+
const at = messages.findIndex(message => message.id === claimedId)
|
|
27
|
+
const current = at >= 0 && messages[at]!.kind === 'custom' ? (messages[at] as CustomMessage) : undefined
|
|
28
|
+
if (current !== undefined) {
|
|
29
|
+
if (definition.update === undefined) return true
|
|
30
|
+
const next = definition.update(event, current)
|
|
31
|
+
if (next === null) {
|
|
32
|
+
messages.splice(at, 1)
|
|
33
|
+
turn.chatNodes.delete(definition.id)
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
messages[at] = { ...next, kind: 'custom', id: current.id }
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
turn.chatNodes.delete(definition.id)
|
|
40
|
+
}
|
|
41
|
+
const message: CustomMessage = { ...definition.start(event), kind: 'custom', id: nextNodeId(definition.id) }
|
|
42
|
+
messages.push(message)
|
|
43
|
+
turn.chatNodes.set(definition.id, message.id)
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applyChatNodes(messages: Message[], event: Parameters<ChatNodeDefinition['match']>[0], turn: TurnState): boolean {
|
|
48
|
+
for (const definition of definitions.values()) {
|
|
49
|
+
if (!definition.match(event)) continue
|
|
50
|
+
return claimChatNode(messages, event, turn, definition)
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { keyedRegistry } from '../kernel/registry.ts'
|
|
2
|
+
import type { InteractionPanelComponentProps, InteractionPanelContribution } from '../contract/index.ts'
|
|
3
|
+
|
|
4
|
+
export type { InteractionPanelComponentProps, InteractionPanelContribution } from '../contract/index.ts'
|
|
5
|
+
|
|
6
|
+
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
|
7
|
+
|
|
8
|
+
export interface ApprovalPanelRequest {
|
|
9
|
+
id: string
|
|
10
|
+
toolName: string
|
|
11
|
+
callId?: string
|
|
12
|
+
reason?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AskOptionLike {
|
|
16
|
+
label: string
|
|
17
|
+
description?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AskQuestionItemLike {
|
|
21
|
+
id: string
|
|
22
|
+
question: string
|
|
23
|
+
header?: string
|
|
24
|
+
detail?: string
|
|
25
|
+
options?: AskOptionLike[]
|
|
26
|
+
multiSelect?: boolean
|
|
27
|
+
multi_select?: boolean
|
|
28
|
+
intent?: { kind?: string; approve?: string }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AskUserQuestionRequestLike {
|
|
32
|
+
questions: AskQuestionItemLike[]
|
|
33
|
+
signal?: AbortSignal
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AskUserQuestionAnswerItemLike {
|
|
37
|
+
id: string
|
|
38
|
+
selected: string[]
|
|
39
|
+
custom?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AskUserQuestionAnswerLike {
|
|
43
|
+
answers: AskUserQuestionAnswerItemLike[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface QuestionPanelRequest {
|
|
47
|
+
id: string
|
|
48
|
+
request: AskUserQuestionRequestLike
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One pending interaction surfaced to the shell. Built-in kinds carry their
|
|
53
|
+
* typed payload in the documented fields; plugin kinds carry anything in
|
|
54
|
+
* `request` and are rendered by the matching panel contribution.
|
|
55
|
+
*/
|
|
56
|
+
export interface ActivePanel {
|
|
57
|
+
kind: string
|
|
58
|
+
/** Built-in approval payload (kind === 'approval'). */
|
|
59
|
+
readonly approval?: ApprovalPanelRequest
|
|
60
|
+
/** Built-in question payload (kind === 'question'). */
|
|
61
|
+
readonly question?: QuestionPanelRequest
|
|
62
|
+
/** Plugin-defined payload for contributions registered under `kind`. */
|
|
63
|
+
readonly request?: unknown
|
|
64
|
+
/** Settle a built-in approval. */
|
|
65
|
+
settle?(outcome: ApprovalOutcome | unknown): void
|
|
66
|
+
/** Resolve the interaction with a value (question or plugin kinds). */
|
|
67
|
+
resolve?(value: unknown): void
|
|
68
|
+
/** Reject the interaction; approvals settle as 'cancelled'. */
|
|
69
|
+
reject?(cause: unknown): void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ApprovalEntry {
|
|
73
|
+
panel: ApprovalPanelRequest
|
|
74
|
+
settle: (value: ApprovalOutcome) => void
|
|
75
|
+
detach: () => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface QuestionEntry {
|
|
79
|
+
panel: QuestionPanelRequest
|
|
80
|
+
resolve: (answer: AskUserQuestionAnswerLike) => void
|
|
81
|
+
reject: (cause: unknown) => void
|
|
82
|
+
detach: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface InteractionPanelRequest {
|
|
86
|
+
kind: string
|
|
87
|
+
request: unknown
|
|
88
|
+
resolve(value: unknown): void
|
|
89
|
+
reject(cause: unknown): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class InteractionPanelRegistry {
|
|
93
|
+
private contributions = keyedRegistry<InteractionPanelContribution>()
|
|
94
|
+
|
|
95
|
+
register(contribution: InteractionPanelContribution): () => void {
|
|
96
|
+
return this.contributions.register(contribution.kind, contribution, { order: contribution.order })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
of(kind: string): InteractionPanelContribution | undefined {
|
|
100
|
+
return this.contributions.get(kind)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let sequence = 0
|
|
105
|
+
|
|
106
|
+
function nextId(prefix: string): string {
|
|
107
|
+
sequence += 1
|
|
108
|
+
return `${prefix}-${Date.now()}-${sequence}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function attachAbort(signal: AbortSignal | undefined, onAbort: () => void): () => void {
|
|
112
|
+
if (signal === undefined) return () => {}
|
|
113
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
114
|
+
return () => {
|
|
115
|
+
signal.removeEventListener('abort', onAbort)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class InteractionStore {
|
|
120
|
+
private listeners = new Set<() => void>()
|
|
121
|
+
private approvals: ApprovalEntry[] = []
|
|
122
|
+
private questions: QuestionEntry[] = []
|
|
123
|
+
private generic: Array<{ entry: InteractionPanelRequest; detach: () => void }> = []
|
|
124
|
+
private snapshot: ActivePanel | null = null
|
|
125
|
+
readonly panels = new InteractionPanelRegistry()
|
|
126
|
+
|
|
127
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
128
|
+
this.listeners.add(listener)
|
|
129
|
+
return () => {
|
|
130
|
+
this.listeners.delete(listener)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getSnapshot = (): ActivePanel | null => this.snapshot
|
|
135
|
+
|
|
136
|
+
private notify(): void {
|
|
137
|
+
for (const listener of [...this.listeners]) listener()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private refresh(): void {
|
|
141
|
+
const approval = this.approvals[0]
|
|
142
|
+
if (approval !== undefined) {
|
|
143
|
+
this.snapshot = {
|
|
144
|
+
kind: 'approval',
|
|
145
|
+
approval: approval.panel,
|
|
146
|
+
settle: outcome => {
|
|
147
|
+
this.settleApproval(approval.panel.id, outcome as ApprovalOutcome)
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const question = this.questions[0]
|
|
152
|
+
if (question !== undefined) {
|
|
153
|
+
this.snapshot = {
|
|
154
|
+
kind: 'question',
|
|
155
|
+
question: question.panel,
|
|
156
|
+
resolve: answer => {
|
|
157
|
+
this.answerQuestion(question.panel.id, answer as AskUserQuestionAnswerLike)
|
|
158
|
+
},
|
|
159
|
+
reject: cause => {
|
|
160
|
+
this.cancelQuestion(question.panel.id, cause instanceof Error ? cause.message : String(cause))
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
const generic = this.generic[0]
|
|
165
|
+
this.snapshot = generic === undefined
|
|
166
|
+
? null
|
|
167
|
+
: { kind: generic.entry.kind, request: generic.entry.request, resolve: generic.entry.resolve, reject: generic.entry.reject }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.notify()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Queue a plugin-defined interaction request. A panel contribution for the
|
|
175
|
+
* given kind must exist for the request to render; the returned promise
|
|
176
|
+
* settles when the panel resolves it, or rejects on abort.
|
|
177
|
+
*/
|
|
178
|
+
pushRequest(kind: string, request: unknown, signal?: AbortSignal): Promise<unknown> {
|
|
179
|
+
if (signal?.aborted === true) return Promise.reject(new Error(`"${kind}" interaction was aborted before the user answered`))
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const entry: InteractionPanelRequest = { kind, request, resolve, reject }
|
|
182
|
+
const detach = attachAbort(signal, () => {
|
|
183
|
+
this.cancelRequest(kind, request, `"${kind}" interaction was aborted before the user answered`)
|
|
184
|
+
})
|
|
185
|
+
this.generic.push({ entry, detach })
|
|
186
|
+
this.refresh()
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resolveRequest(kind: string, request: unknown, value: unknown): boolean {
|
|
191
|
+
const index = this.generic.findIndex(slot => slot.entry.kind === kind && slot.entry.request === request)
|
|
192
|
+
if (index < 0) return false
|
|
193
|
+
const [slot] = this.generic.splice(index, 1)
|
|
194
|
+
slot.detach()
|
|
195
|
+
slot.entry.resolve(value)
|
|
196
|
+
this.refresh()
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
cancelRequest(kind: string, request: unknown, message: string): boolean {
|
|
201
|
+
const index = this.generic.findIndex(slot => slot.entry.kind === kind && slot.entry.request === request)
|
|
202
|
+
if (index < 0) return false
|
|
203
|
+
const [slot] = this.generic.splice(index, 1)
|
|
204
|
+
slot.detach()
|
|
205
|
+
slot.entry.reject(new Error(message))
|
|
206
|
+
this.refresh()
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pushApproval(toolName: string, callId?: string, reason?: string, signal?: AbortSignal): Promise<ApprovalOutcome> {
|
|
211
|
+
if (signal?.aborted === true) return Promise.resolve('cancelled')
|
|
212
|
+
return new Promise(resolve => {
|
|
213
|
+
const panel: ApprovalPanelRequest = {
|
|
214
|
+
id: nextId('approval'),
|
|
215
|
+
toolName,
|
|
216
|
+
...(callId === undefined ? {} : { callId }),
|
|
217
|
+
...(reason === undefined ? {} : { reason }),
|
|
218
|
+
}
|
|
219
|
+
const detach = attachAbort(signal, () => {
|
|
220
|
+
this.settleApproval(panel.id, 'cancelled')
|
|
221
|
+
})
|
|
222
|
+
this.approvals.push({ panel, settle: resolve, detach })
|
|
223
|
+
this.refresh()
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
settleApproval(id: string, outcome: ApprovalOutcome): boolean {
|
|
228
|
+
const index = this.approvals.findIndex(entry => entry.panel.id === id)
|
|
229
|
+
if (index < 0) return false
|
|
230
|
+
const [entry] = this.approvals.splice(index, 1)
|
|
231
|
+
entry.detach()
|
|
232
|
+
entry.settle(outcome)
|
|
233
|
+
this.refresh()
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
pushQuestion(rawRequest: AskUserQuestionRequestLike, signal?: AbortSignal): Promise<AskUserQuestionAnswerLike> {
|
|
238
|
+
const request: AskUserQuestionRequestLike = {
|
|
239
|
+
...rawRequest,
|
|
240
|
+
questions: rawRequest.questions.map(question => ({
|
|
241
|
+
...question,
|
|
242
|
+
multiSelect: question.multiSelect ?? question.multi_select,
|
|
243
|
+
})),
|
|
244
|
+
}
|
|
245
|
+
if (request.questions.length === 0) {
|
|
246
|
+
return Promise.reject(new Error('no questions were provided'))
|
|
247
|
+
}
|
|
248
|
+
if (signal?.aborted === true) {
|
|
249
|
+
return Promise.reject(new Error('ask_user_question was aborted before the user answered'))
|
|
250
|
+
}
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const panel: QuestionPanelRequest = { id: nextId('question'), request }
|
|
253
|
+
const detach = attachAbort(signal, () => {
|
|
254
|
+
this.cancelQuestion(panel.id, 'ask_user_question was aborted before the user answered')
|
|
255
|
+
})
|
|
256
|
+
this.questions.push({ panel, resolve, reject, detach })
|
|
257
|
+
this.refresh()
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
answerQuestion(id: string, answer: AskUserQuestionAnswerLike): boolean {
|
|
262
|
+
const index = this.questions.findIndex(entry => entry.panel.id === id)
|
|
263
|
+
if (index < 0) return false
|
|
264
|
+
const [entry] = this.questions.splice(index, 1)
|
|
265
|
+
entry.detach()
|
|
266
|
+
entry.resolve(answer)
|
|
267
|
+
this.refresh()
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
cancelQuestion(id: string, message: string): boolean {
|
|
272
|
+
const index = this.questions.findIndex(entry => entry.panel.id === id)
|
|
273
|
+
if (index < 0) return false
|
|
274
|
+
const [entry] = this.questions.splice(index, 1)
|
|
275
|
+
entry.detach()
|
|
276
|
+
entry.reject(new Error(message))
|
|
277
|
+
this.refresh()
|
|
278
|
+
return true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
dispose(): void {
|
|
282
|
+
const approvals = this.approvals.splice(0)
|
|
283
|
+
for (const entry of approvals) {
|
|
284
|
+
entry.detach()
|
|
285
|
+
entry.settle('cancelled')
|
|
286
|
+
}
|
|
287
|
+
const questions = this.questions.splice(0)
|
|
288
|
+
for (const entry of questions) {
|
|
289
|
+
entry.detach()
|
|
290
|
+
entry.reject(new Error('the interaction store was disposed'))
|
|
291
|
+
}
|
|
292
|
+
const generic = this.generic.splice(0)
|
|
293
|
+
for (const slot of generic) {
|
|
294
|
+
slot.detach()
|
|
295
|
+
slot.entry.reject(new Error('the interaction store was disposed'))
|
|
296
|
+
}
|
|
297
|
+
this.refresh()
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type ApprovalRequestLike = {
|
|
302
|
+
agent: unknown
|
|
303
|
+
toolName: string
|
|
304
|
+
callId?: string
|
|
305
|
+
reason?: string
|
|
306
|
+
signal?: AbortSignal
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type CtxLike = {
|
|
310
|
+
on(event: 'approval/request', handler: (req: ApprovalRequestLike) => Promise<ApprovalOutcome>): () => void
|
|
311
|
+
get(service: 'userQuestions'): {
|
|
312
|
+
registerProvider(provider: { ask(request: AskUserQuestionRequestLike): Promise<AskUserQuestionAnswerLike> }): () => void
|
|
313
|
+
} | undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function registerInteractionChannels(ctx: CtxLike, store: InteractionStore): () => void {
|
|
317
|
+
const offApproval = ctx.on('approval/request', async req => {
|
|
318
|
+
return store.pushApproval(req.toolName, req.callId, req.reason, req.signal)
|
|
319
|
+
})
|
|
320
|
+
const unregisterProvider = ctx.get('userQuestions')?.registerProvider({
|
|
321
|
+
ask(request) {
|
|
322
|
+
return store.pushQuestion(request, request.signal)
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
return () => {
|
|
326
|
+
offApproval()
|
|
327
|
+
unregisterProvider?.()
|
|
328
|
+
}
|
|
329
|
+
}
|