pi-learning-agent 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/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/SECURITY.md +9 -0
- package/docs/architecture.md +75 -0
- package/docs/images/learning-workspace.png +0 -0
- package/docs/releasing.md +51 -0
- package/extension/commands.ts +181 -0
- package/extension/index.ts +50 -0
- package/extension/learning-session.ts +75 -0
- package/extension/runner/code-runner.ts +275 -0
- package/extension/server/interaction-broker.ts +345 -0
- package/extension/server/learning-server.ts +753 -0
- package/extension/server/sse-hub.ts +63 -0
- package/extension/state/learning-state.ts +458 -0
- package/extension/state/session-persistence.ts +72 -0
- package/extension/tools/ask-code.ts +97 -0
- package/extension/tools/ask-free-response.ts +88 -0
- package/extension/tools/ask-multi-choice.ts +109 -0
- package/extension/tools/ask-record-attempt.ts +50 -0
- package/extension/tools/ask-single-choice.ts +110 -0
- package/extension/tools/index.ts +39 -0
- package/extension/tools/tui-presenter.ts +609 -0
- package/extension/transcript-sync.ts +197 -0
- package/extension/tutor-prompt.ts +64 -0
- package/extension/utils/browser.ts +35 -0
- package/extension/utils/ids.ts +5 -0
- package/extension/utils/validation.ts +138 -0
- package/package.json +82 -0
- package/shared/learning-state.ts +62 -0
- package/shared/protocol.ts +236 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/abap-08VXUWAP.js +1 -0
- package/web/dist/assets/apex-BWPQTe0t.js +1 -0
- package/web/dist/assets/azcli-Bc_sGQ0U.js +1 -0
- package/web/dist/assets/bat-i0X4ZdIN.js +1 -0
- package/web/dist/assets/bicep-B5-_aFwp.js +2 -0
- package/web/dist/assets/cameligo-DMUM7wLl.js +1 -0
- package/web/dist/assets/clojure-Cm7r79vr.js +1 -0
- package/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/web/dist/assets/coffee-Ba7i2nA0.js +1 -0
- package/web/dist/assets/cpp-C7h46wYY.js +1 -0
- package/web/dist/assets/csharp-BKxtCVv1.js +1 -0
- package/web/dist/assets/csp-bTuwJoIa.js +1 -0
- package/web/dist/assets/css-DIMkf-bt.js +3 -0
- package/web/dist/assets/css.worker-DRH2OcAG.js +93 -0
- package/web/dist/assets/cssMode-BpvqYhpN.js +1 -0
- package/web/dist/assets/cypher-CVaqCwHa.js +1 -0
- package/web/dist/assets/dart-onAF5SnQ.js +1 -0
- package/web/dist/assets/dockerfile-DZFCIeNp.js +1 -0
- package/web/dist/assets/ecl-D05T4iGw.js +1 -0
- package/web/dist/assets/editor.worker-D9zwrD0f.js +30 -0
- package/web/dist/assets/elixir-6RTg0lbw.js +1 -0
- package/web/dist/assets/flow9-C5_-GSwl.js +1 -0
- package/web/dist/assets/freemarker2-CmTivYCM.js +3 -0
- package/web/dist/assets/fsharp-C8Ef5oNN.js +1 -0
- package/web/dist/assets/go-C-y9NEjX.js +1 -0
- package/web/dist/assets/graphql-fmXr3nnJ.js +1 -0
- package/web/dist/assets/handlebars-ClhpojlY.js +1 -0
- package/web/dist/assets/hcl-CpzslTdj.js +1 -0
- package/web/dist/assets/html-CTqIxFH7.js +1 -0
- package/web/dist/assets/html.worker-DQLddT9h.js +506 -0
- package/web/dist/assets/htmlMode-BKCTQ4_a.js +1 -0
- package/web/dist/assets/index-6N_lnfyE.js +1243 -0
- package/web/dist/assets/index-B5x0MzHm.css +1 -0
- package/web/dist/assets/ini-sBoK_t0W.js +1 -0
- package/web/dist/assets/java-BEtHBSE6.js +1 -0
- package/web/dist/assets/javascript-Cp_q6aWP.js +1 -0
- package/web/dist/assets/json.worker-D_Dqrls5.js +62 -0
- package/web/dist/assets/jsonMode-CMDRrSMX.js +7 -0
- package/web/dist/assets/julia-Bri6UV-V.js +1 -0
- package/web/dist/assets/kotlin-BOotOW0E.js +1 -0
- package/web/dist/assets/less-B9JPFI3C.js +2 -0
- package/web/dist/assets/lexon-CfSJPG6W.js +1 -0
- package/web/dist/assets/liquid-BFK8j46k.js +1 -0
- package/web/dist/assets/lspLanguageFeatures-nT2b0EDJ.js +4 -0
- package/web/dist/assets/lua-CsQS60Ue.js +1 -0
- package/web/dist/assets/m3-D-oSqn_W.js +1 -0
- package/web/dist/assets/markdown-Cimd5fb3.js +1 -0
- package/web/dist/assets/mdx-cEJtnt-p.js +1 -0
- package/web/dist/assets/mips-CIPQ_RoX.js +1 -0
- package/web/dist/assets/msdax-DauUninz.js +1 -0
- package/web/dist/assets/mysql-SOo6toE5.js +1 -0
- package/web/dist/assets/objective-c-FvmIjYaQ.js +1 -0
- package/web/dist/assets/pascal-DrH0SRf2.js +1 -0
- package/web/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
- package/web/dist/assets/perl-oz_6vUea.js +1 -0
- package/web/dist/assets/pgsql-DTj74zXo.js +1 -0
- package/web/dist/assets/php-nr791fC2.js +1 -0
- package/web/dist/assets/pla-CopQ2nXW.js +1 -0
- package/web/dist/assets/postiats-43DmfD33.js +1 -0
- package/web/dist/assets/powerquery-D3hlyOfw.js +1 -0
- package/web/dist/assets/powershell-DmHpPYUd.js +1 -0
- package/web/dist/assets/protobuf-C531GsRP.js +2 -0
- package/web/dist/assets/pug-Z5eAx3Zn.js +1 -0
- package/web/dist/assets/python-bhI_n2_n.js +1 -0
- package/web/dist/assets/qsharp-DkqhCAOL.js +1 -0
- package/web/dist/assets/r-BwWrilGY.js +1 -0
- package/web/dist/assets/razor-D40-I8aA.js +1 -0
- package/web/dist/assets/redis-ClamHrr6.js +1 -0
- package/web/dist/assets/redshift-DT7zqm-g.js +1 -0
- package/web/dist/assets/restructuredtext-BYgofb2h.js +1 -0
- package/web/dist/assets/ruby-DezsRK8O.js +1 -0
- package/web/dist/assets/rust-DdL9SqIa.js +1 -0
- package/web/dist/assets/sb-CcwsVR0C.js +1 -0
- package/web/dist/assets/scala-DHpiXF5c.js +1 -0
- package/web/dist/assets/scheme-BeGwcela.js +1 -0
- package/web/dist/assets/scss-gp-XZpBa.js +3 -0
- package/web/dist/assets/shell-CC2rA5mh.js +1 -0
- package/web/dist/assets/solidity-BEEn4gHE.js +1 -0
- package/web/dist/assets/sophia-CRfGWb83.js +1 -0
- package/web/dist/assets/sparql-D_Lu-MrJ.js +1 -0
- package/web/dist/assets/sql-NEE52Syq.js +1 -0
- package/web/dist/assets/st-DbInun42.js +1 -0
- package/web/dist/assets/swift-Bxkupp3x.js +1 -0
- package/web/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
- package/web/dist/assets/tcl-DISqw1ZD.js +1 -0
- package/web/dist/assets/ts.worker-DkYoA6bF.js +67738 -0
- package/web/dist/assets/tsMode-C9OJ2Aji.js +11 -0
- package/web/dist/assets/twig-De2hgUGE.js +1 -0
- package/web/dist/assets/typescript-CZ5RbSdB.js +1 -0
- package/web/dist/assets/typespec-B8J7ngcE.js +1 -0
- package/web/dist/assets/vb-DV3o63ZY.js +1 -0
- package/web/dist/assets/wgsl-DpFanUEy.js +298 -0
- package/web/dist/assets/workers-qb_EZhxe.js +1 -0
- package/web/dist/assets/xml-BdDnAXLl.js +1 -0
- package/web/dist/assets/yaml-BSYHoVQy.js +1 -0
- package/web/dist/index.html +23 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
getSelectListTheme,
|
|
4
|
+
type ExtensionContext
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import type { InteractionBroker } from "../server/interaction-broker.js";
|
|
8
|
+
import type {
|
|
9
|
+
CodeExerciseInteraction,
|
|
10
|
+
CodeExerciseResolvedAnswer,
|
|
11
|
+
FreeResponseInteraction,
|
|
12
|
+
FreeResponseResolvedAnswer,
|
|
13
|
+
LearningInteraction,
|
|
14
|
+
MultiChoiceInteraction,
|
|
15
|
+
MultiChoiceResolvedAnswer,
|
|
16
|
+
ResolvedAnswer,
|
|
17
|
+
SingleChoiceInteraction,
|
|
18
|
+
SingleChoiceResolvedAnswer,
|
|
19
|
+
SkippedResolvedAnswer
|
|
20
|
+
} from "../../shared/protocol.js";
|
|
21
|
+
|
|
22
|
+
/** Single/multi choice answers may end as a structured skip (spec 7.5). */
|
|
23
|
+
export type ChoiceResolvedAnswer =
|
|
24
|
+
| SingleChoiceResolvedAnswer
|
|
25
|
+
| SkippedResolvedAnswer;
|
|
26
|
+
|
|
27
|
+
export type MultiChoiceResolvedAnswerOrSkip =
|
|
28
|
+
| MultiChoiceResolvedAnswer
|
|
29
|
+
| SkippedResolvedAnswer;
|
|
30
|
+
|
|
31
|
+
export interface TuiLearningPresenter {
|
|
32
|
+
presentSingleChoice(
|
|
33
|
+
interaction: SingleChoiceInteraction,
|
|
34
|
+
signal: AbortSignal | undefined,
|
|
35
|
+
ctx: ExtensionContext
|
|
36
|
+
): Promise<ChoiceResolvedAnswer>;
|
|
37
|
+
presentMultiChoice(
|
|
38
|
+
interaction: MultiChoiceInteraction,
|
|
39
|
+
signal: AbortSignal | undefined,
|
|
40
|
+
ctx: ExtensionContext
|
|
41
|
+
): Promise<MultiChoiceResolvedAnswerOrSkip>;
|
|
42
|
+
presentFreeResponse(
|
|
43
|
+
interaction: FreeResponseInteraction,
|
|
44
|
+
signal: AbortSignal | undefined,
|
|
45
|
+
ctx: ExtensionContext
|
|
46
|
+
): Promise<FreeResponseResolvedAnswer>;
|
|
47
|
+
presentCode(
|
|
48
|
+
interaction: CodeExerciseInteraction,
|
|
49
|
+
signal: AbortSignal | undefined,
|
|
50
|
+
ctx: ExtensionContext
|
|
51
|
+
): Promise<CodeExerciseResolvedAnswer>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createTuiPresenter(now: () => number = Date.now): TuiLearningPresenter {
|
|
55
|
+
return {
|
|
56
|
+
presentSingleChoice: (interaction, signal, ctx) =>
|
|
57
|
+
presentSingleChoiceInTui(interaction, signal, ctx, now),
|
|
58
|
+
presentMultiChoice: (interaction, signal, ctx) =>
|
|
59
|
+
presentMultiChoiceInTui(interaction, signal, ctx, now),
|
|
60
|
+
presentFreeResponse: (interaction, signal, ctx) =>
|
|
61
|
+
presentFreeResponseInTui(interaction, signal, ctx, now),
|
|
62
|
+
presentCode: (interaction, signal, ctx) =>
|
|
63
|
+
presentCodeInTui(interaction, signal, ctx, now)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createBrokerBackedTuiPresenter(
|
|
68
|
+
broker: InteractionBroker,
|
|
69
|
+
now: () => number = Date.now
|
|
70
|
+
): TuiLearningPresenter {
|
|
71
|
+
const tui = createTuiPresenter(now);
|
|
72
|
+
return {
|
|
73
|
+
presentSingleChoice: (interaction, signal, ctx) =>
|
|
74
|
+
presentThroughBroker(broker, interaction, signal, (combinedSignal) =>
|
|
75
|
+
tui.presentSingleChoice(interaction, combinedSignal, ctx)
|
|
76
|
+
),
|
|
77
|
+
presentMultiChoice: (interaction, signal, ctx) =>
|
|
78
|
+
presentThroughBroker(broker, interaction, signal, (combinedSignal) =>
|
|
79
|
+
tui.presentMultiChoice(interaction, combinedSignal, ctx)
|
|
80
|
+
),
|
|
81
|
+
presentFreeResponse: (interaction, signal, ctx) =>
|
|
82
|
+
presentThroughBroker(broker, interaction, signal, (combinedSignal) =>
|
|
83
|
+
tui.presentFreeResponse(interaction, combinedSignal, ctx)
|
|
84
|
+
),
|
|
85
|
+
presentCode: (interaction, signal, ctx) =>
|
|
86
|
+
presentThroughBroker(broker, interaction, signal, (combinedSignal) =>
|
|
87
|
+
tui.presentCode(interaction, combinedSignal, ctx)
|
|
88
|
+
)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* uiMode auto (spec 28): with a web client the interaction is presented to
|
|
94
|
+
* the broker only and waits for the browser to submit via the server; the
|
|
95
|
+
* TUI is not touched. Without a web client, fully reuses
|
|
96
|
+
* createBrokerBackedTuiPresenter (broker + TUI race).
|
|
97
|
+
*/
|
|
98
|
+
export function createModeAwarePresenter(
|
|
99
|
+
broker: InteractionBroker,
|
|
100
|
+
hasWebClient: () => boolean,
|
|
101
|
+
now: () => number = Date.now
|
|
102
|
+
): TuiLearningPresenter {
|
|
103
|
+
const fallback = createBrokerBackedTuiPresenter(broker, now);
|
|
104
|
+
return {
|
|
105
|
+
presentSingleChoice: (interaction, signal, ctx) =>
|
|
106
|
+
hasWebClient()
|
|
107
|
+
? presentViaBrokerWithFallback(
|
|
108
|
+
broker,
|
|
109
|
+
hasWebClient,
|
|
110
|
+
interaction,
|
|
111
|
+
signal,
|
|
112
|
+
() => fallback.presentSingleChoice(interaction, signal, ctx)
|
|
113
|
+
)
|
|
114
|
+
: fallback.presentSingleChoice(interaction, signal, ctx),
|
|
115
|
+
presentMultiChoice: (interaction, signal, ctx) =>
|
|
116
|
+
hasWebClient()
|
|
117
|
+
? presentViaBrokerWithFallback(
|
|
118
|
+
broker,
|
|
119
|
+
hasWebClient,
|
|
120
|
+
interaction,
|
|
121
|
+
signal,
|
|
122
|
+
() => fallback.presentMultiChoice(interaction, signal, ctx)
|
|
123
|
+
)
|
|
124
|
+
: fallback.presentMultiChoice(interaction, signal, ctx),
|
|
125
|
+
presentFreeResponse: (interaction, signal, ctx) =>
|
|
126
|
+
hasWebClient()
|
|
127
|
+
? presentViaBrokerWithFallback(
|
|
128
|
+
broker,
|
|
129
|
+
hasWebClient,
|
|
130
|
+
interaction,
|
|
131
|
+
signal,
|
|
132
|
+
() => fallback.presentFreeResponse(interaction, signal, ctx)
|
|
133
|
+
)
|
|
134
|
+
: fallback.presentFreeResponse(interaction, signal, ctx),
|
|
135
|
+
presentCode: (interaction, signal, ctx) =>
|
|
136
|
+
hasWebClient()
|
|
137
|
+
? presentViaBrokerWithFallback(
|
|
138
|
+
broker,
|
|
139
|
+
hasWebClient,
|
|
140
|
+
interaction,
|
|
141
|
+
signal,
|
|
142
|
+
() => fallback.presentCode(interaction, signal, ctx)
|
|
143
|
+
)
|
|
144
|
+
: fallback.presentCode(interaction, signal, ctx)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const WEB_DISCONNECT_POLL_MS = 250;
|
|
149
|
+
|
|
150
|
+
function presentViaBroker(
|
|
151
|
+
broker: InteractionBroker,
|
|
152
|
+
interaction: SingleChoiceInteraction,
|
|
153
|
+
signal: AbortSignal | undefined
|
|
154
|
+
): Promise<ChoiceResolvedAnswer>;
|
|
155
|
+
function presentViaBroker(
|
|
156
|
+
broker: InteractionBroker,
|
|
157
|
+
interaction: MultiChoiceInteraction,
|
|
158
|
+
signal: AbortSignal | undefined
|
|
159
|
+
): Promise<MultiChoiceResolvedAnswerOrSkip>;
|
|
160
|
+
function presentViaBroker(
|
|
161
|
+
broker: InteractionBroker,
|
|
162
|
+
interaction: FreeResponseInteraction,
|
|
163
|
+
signal: AbortSignal | undefined
|
|
164
|
+
): Promise<FreeResponseResolvedAnswer>;
|
|
165
|
+
function presentViaBroker(
|
|
166
|
+
broker: InteractionBroker,
|
|
167
|
+
interaction: CodeExerciseInteraction,
|
|
168
|
+
signal: AbortSignal | undefined
|
|
169
|
+
): Promise<CodeExerciseResolvedAnswer>;
|
|
170
|
+
function presentViaBroker(
|
|
171
|
+
broker: InteractionBroker,
|
|
172
|
+
interaction: LearningInteraction,
|
|
173
|
+
signal: AbortSignal | undefined
|
|
174
|
+
): Promise<ResolvedAnswer> {
|
|
175
|
+
// Web-only path: the browser submits through the server; broker.present
|
|
176
|
+
// rejects on tool abort, so the tool surfaces the cancellation normally.
|
|
177
|
+
return broker.present(interaction, signal);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function presentViaBrokerWithFallback(
|
|
181
|
+
broker: InteractionBroker,
|
|
182
|
+
hasWebClient: () => boolean,
|
|
183
|
+
interaction: SingleChoiceInteraction,
|
|
184
|
+
signal: AbortSignal | undefined,
|
|
185
|
+
fallback: () => Promise<ChoiceResolvedAnswer>
|
|
186
|
+
): Promise<ChoiceResolvedAnswer>;
|
|
187
|
+
function presentViaBrokerWithFallback(
|
|
188
|
+
broker: InteractionBroker,
|
|
189
|
+
hasWebClient: () => boolean,
|
|
190
|
+
interaction: MultiChoiceInteraction,
|
|
191
|
+
signal: AbortSignal | undefined,
|
|
192
|
+
fallback: () => Promise<MultiChoiceResolvedAnswerOrSkip>
|
|
193
|
+
): Promise<MultiChoiceResolvedAnswerOrSkip>;
|
|
194
|
+
function presentViaBrokerWithFallback(
|
|
195
|
+
broker: InteractionBroker,
|
|
196
|
+
hasWebClient: () => boolean,
|
|
197
|
+
interaction: FreeResponseInteraction,
|
|
198
|
+
signal: AbortSignal | undefined,
|
|
199
|
+
fallback: () => Promise<FreeResponseResolvedAnswer>
|
|
200
|
+
): Promise<FreeResponseResolvedAnswer>;
|
|
201
|
+
function presentViaBrokerWithFallback(
|
|
202
|
+
broker: InteractionBroker,
|
|
203
|
+
hasWebClient: () => boolean,
|
|
204
|
+
interaction: CodeExerciseInteraction,
|
|
205
|
+
signal: AbortSignal | undefined,
|
|
206
|
+
fallback: () => Promise<CodeExerciseResolvedAnswer>
|
|
207
|
+
): Promise<CodeExerciseResolvedAnswer>;
|
|
208
|
+
function presentViaBrokerWithFallback(
|
|
209
|
+
broker: InteractionBroker,
|
|
210
|
+
hasWebClient: () => boolean,
|
|
211
|
+
interaction: LearningInteraction,
|
|
212
|
+
signal: AbortSignal | undefined,
|
|
213
|
+
fallback: () => Promise<ResolvedAnswer>
|
|
214
|
+
): Promise<ResolvedAnswer> {
|
|
215
|
+
return new Promise<ResolvedAnswer>((resolve, reject) => {
|
|
216
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
217
|
+
let switchingToTui = false;
|
|
218
|
+
let settled = false;
|
|
219
|
+
const cleanup = () => {
|
|
220
|
+
if (timer !== undefined) {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
timer = undefined;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const resolveOnce = (answer: ResolvedAnswer) => {
|
|
226
|
+
if (settled) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
settled = true;
|
|
230
|
+
cleanup();
|
|
231
|
+
resolve(answer);
|
|
232
|
+
};
|
|
233
|
+
const rejectOnce = (error: unknown) => {
|
|
234
|
+
if (settled) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
settled = true;
|
|
238
|
+
cleanup();
|
|
239
|
+
reject(error);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
void broker.present(interaction, signal).then(
|
|
243
|
+
(answer) => {
|
|
244
|
+
if (!switchingToTui) {
|
|
245
|
+
resolveOnce(answer);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
(error: unknown) => {
|
|
249
|
+
// broker.cancel("web_disconnected") is expected during the handoff;
|
|
250
|
+
// the fallback presenter now owns the interaction result.
|
|
251
|
+
if (!switchingToTui) {
|
|
252
|
+
rejectOnce(error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const poll = () => {
|
|
258
|
+
if (settled || switchingToTui) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!hasWebClient()) {
|
|
262
|
+
const stillPending = broker
|
|
263
|
+
.getPending()
|
|
264
|
+
.some((pending) => pending.id === interaction.id);
|
|
265
|
+
if (!stillPending) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
switchingToTui = true;
|
|
269
|
+
// Reject the Web wait before opening the TUI so only one broker entry
|
|
270
|
+
// can own this interaction at a time.
|
|
271
|
+
broker.cancel(interaction.id, "web_disconnected");
|
|
272
|
+
void fallback().then(resolveOnce, rejectOnce);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
timer = setTimeout(poll, WEB_DISCONNECT_POLL_MS);
|
|
276
|
+
};
|
|
277
|
+
timer = setTimeout(poll, WEB_DISCONNECT_POLL_MS);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function presentSingleChoiceInTui(
|
|
282
|
+
interaction: SingleChoiceInteraction,
|
|
283
|
+
signal: AbortSignal | undefined,
|
|
284
|
+
ctx: ExtensionContext,
|
|
285
|
+
now: () => number
|
|
286
|
+
): Promise<ChoiceResolvedAnswer> {
|
|
287
|
+
requireUi(ctx, "Single-choice learning");
|
|
288
|
+
throwIfAborted(interaction.id, signal);
|
|
289
|
+
|
|
290
|
+
const choices = interaction.options.map(
|
|
291
|
+
(option, index) => `${index + 1}. ${option.label}`
|
|
292
|
+
);
|
|
293
|
+
const skipItem = "跳过此题";
|
|
294
|
+
const menu = interaction.allowSkip ? [...choices, skipItem] : choices;
|
|
295
|
+
const selected = await ctx.ui.select(
|
|
296
|
+
interaction.title === undefined
|
|
297
|
+
? interaction.question
|
|
298
|
+
: `${interaction.title}\n${interaction.question}`,
|
|
299
|
+
menu,
|
|
300
|
+
signal === undefined ? undefined : { signal }
|
|
301
|
+
);
|
|
302
|
+
throwIfAborted(interaction.id, signal);
|
|
303
|
+
if (selected === undefined) {
|
|
304
|
+
throw new Error(`Learner cancelled interaction ${interaction.id}.`);
|
|
305
|
+
}
|
|
306
|
+
if (selected === skipItem) {
|
|
307
|
+
return {
|
|
308
|
+
interactionId: interaction.id,
|
|
309
|
+
type: interaction.type,
|
|
310
|
+
skipped: true,
|
|
311
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const option = interaction.options[choices.indexOf(selected)];
|
|
316
|
+
if (option === undefined) {
|
|
317
|
+
throw new Error(`TUI returned an unknown choice for ${interaction.id}.`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
interactionId: interaction.id,
|
|
322
|
+
type: interaction.type,
|
|
323
|
+
answer: { optionId: option.id },
|
|
324
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Multi-select via repeated ctx.ui.select dialogs: pi-tui's SelectList has no
|
|
330
|
+
* multiSelect mode and pi-tui ships no checkbox component, so each option is
|
|
331
|
+
* picked in its own dialog; a trailing “done” entry finishes the loop.
|
|
332
|
+
* Selecting an already-picked option toggles it off. Esc cancels.
|
|
333
|
+
*/
|
|
334
|
+
export async function presentMultiChoiceInTui(
|
|
335
|
+
interaction: MultiChoiceInteraction,
|
|
336
|
+
signal: AbortSignal | undefined,
|
|
337
|
+
ctx: ExtensionContext,
|
|
338
|
+
now: () => number
|
|
339
|
+
): Promise<MultiChoiceResolvedAnswerOrSkip> {
|
|
340
|
+
requireUi(ctx, "Multi-choice learning");
|
|
341
|
+
throwIfAborted(interaction.id, signal);
|
|
342
|
+
|
|
343
|
+
const baseTitle =
|
|
344
|
+
interaction.title === undefined
|
|
345
|
+
? interaction.question
|
|
346
|
+
: `${interaction.title}\n${interaction.question}`;
|
|
347
|
+
const choices = interaction.options.map(
|
|
348
|
+
(option, index) => `${index + 1}. ${option.label}`
|
|
349
|
+
);
|
|
350
|
+
const doneItem = "✔ 完成";
|
|
351
|
+
const skipItem = "跳过此题";
|
|
352
|
+
const menu = interaction.allowSkip
|
|
353
|
+
? [...choices, doneItem, skipItem]
|
|
354
|
+
: [...choices, doneItem];
|
|
355
|
+
const selected = new Set<string>();
|
|
356
|
+
|
|
357
|
+
for (;;) {
|
|
358
|
+
const prompt =
|
|
359
|
+
selected.size === 0
|
|
360
|
+
? `${baseTitle}\n(可能有多个正确答案,可多次选择)`
|
|
361
|
+
: `${baseTitle}\n(已选 ${selected.size} 项,可继续选择,选“✔ 完成”结束)`;
|
|
362
|
+
const pick = await ctx.ui.select(
|
|
363
|
+
prompt,
|
|
364
|
+
menu,
|
|
365
|
+
signal === undefined ? undefined : { signal }
|
|
366
|
+
);
|
|
367
|
+
throwIfAborted(interaction.id, signal);
|
|
368
|
+
if (pick === undefined) {
|
|
369
|
+
throw new Error(`Learner cancelled interaction ${interaction.id}.`);
|
|
370
|
+
}
|
|
371
|
+
if (pick === skipItem) {
|
|
372
|
+
return {
|
|
373
|
+
interactionId: interaction.id,
|
|
374
|
+
type: interaction.type,
|
|
375
|
+
skipped: true,
|
|
376
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (pick === doneItem) {
|
|
380
|
+
if (selected.size === 0) {
|
|
381
|
+
ctx.ui.notify("至少选择一个选项后再完成。", "warning");
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
const option = interaction.options[choices.indexOf(pick)];
|
|
387
|
+
if (option === undefined) {
|
|
388
|
+
throw new Error(`TUI returned an unknown choice for ${interaction.id}.`);
|
|
389
|
+
}
|
|
390
|
+
if (selected.has(option.id)) {
|
|
391
|
+
selected.delete(option.id);
|
|
392
|
+
} else {
|
|
393
|
+
selected.add(option.id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
interactionId: interaction.id,
|
|
399
|
+
type: interaction.type,
|
|
400
|
+
answer: { optionIds: [...selected] },
|
|
401
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function presentFreeResponseInTui(
|
|
406
|
+
interaction: FreeResponseInteraction,
|
|
407
|
+
signal: AbortSignal | undefined,
|
|
408
|
+
ctx: ExtensionContext,
|
|
409
|
+
now: () => number
|
|
410
|
+
): Promise<FreeResponseResolvedAnswer> {
|
|
411
|
+
requireUi(ctx, "Free-response learning");
|
|
412
|
+
throwIfAborted(interaction.id, signal);
|
|
413
|
+
|
|
414
|
+
const text = interaction.multiline
|
|
415
|
+
? await presentMultilineEditor(ctx, interaction.question, "", signal)
|
|
416
|
+
: await ctx.ui.input(
|
|
417
|
+
interaction.question,
|
|
418
|
+
interaction.placeholder,
|
|
419
|
+
signal === undefined ? undefined : { signal }
|
|
420
|
+
);
|
|
421
|
+
throwIfAborted(interaction.id, signal);
|
|
422
|
+
if (text === undefined) {
|
|
423
|
+
throw new Error(`Learner cancelled interaction ${interaction.id}.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
interactionId: interaction.id,
|
|
428
|
+
type: interaction.type,
|
|
429
|
+
answer: { text },
|
|
430
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function presentCodeInTui(
|
|
435
|
+
interaction: CodeExerciseInteraction,
|
|
436
|
+
signal: AbortSignal | undefined,
|
|
437
|
+
ctx: ExtensionContext,
|
|
438
|
+
now: () => number
|
|
439
|
+
): Promise<CodeExerciseResolvedAnswer> {
|
|
440
|
+
requireUi(ctx, "Code learning");
|
|
441
|
+
throwIfAborted(interaction.id, signal);
|
|
442
|
+
|
|
443
|
+
const code = await presentMultilineEditor(
|
|
444
|
+
ctx,
|
|
445
|
+
interaction.title === undefined
|
|
446
|
+
? interaction.instructions
|
|
447
|
+
: `${interaction.title}\n${interaction.instructions}`,
|
|
448
|
+
interaction.starterCode,
|
|
449
|
+
signal
|
|
450
|
+
);
|
|
451
|
+
throwIfAborted(interaction.id, signal);
|
|
452
|
+
if (code === undefined) {
|
|
453
|
+
throw new Error(`Learner cancelled interaction ${interaction.id}.`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
interactionId: interaction.id,
|
|
458
|
+
type: interaction.type,
|
|
459
|
+
answer: { language: interaction.language, code },
|
|
460
|
+
responseTimeMs: elapsedSince(interaction.createdAt, now)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function presentThroughBroker(
|
|
465
|
+
broker: InteractionBroker,
|
|
466
|
+
interaction: SingleChoiceInteraction,
|
|
467
|
+
signal: AbortSignal | undefined,
|
|
468
|
+
collect: (signal: AbortSignal) => Promise<ChoiceResolvedAnswer>
|
|
469
|
+
): Promise<ChoiceResolvedAnswer>;
|
|
470
|
+
function presentThroughBroker(
|
|
471
|
+
broker: InteractionBroker,
|
|
472
|
+
interaction: MultiChoiceInteraction,
|
|
473
|
+
signal: AbortSignal | undefined,
|
|
474
|
+
collect: (signal: AbortSignal) => Promise<MultiChoiceResolvedAnswerOrSkip>
|
|
475
|
+
): Promise<MultiChoiceResolvedAnswerOrSkip>;
|
|
476
|
+
function presentThroughBroker(
|
|
477
|
+
broker: InteractionBroker,
|
|
478
|
+
interaction: FreeResponseInteraction,
|
|
479
|
+
signal: AbortSignal | undefined,
|
|
480
|
+
collect: (signal: AbortSignal) => Promise<FreeResponseResolvedAnswer>
|
|
481
|
+
): Promise<FreeResponseResolvedAnswer>;
|
|
482
|
+
function presentThroughBroker(
|
|
483
|
+
broker: InteractionBroker,
|
|
484
|
+
interaction: CodeExerciseInteraction,
|
|
485
|
+
signal: AbortSignal | undefined,
|
|
486
|
+
collect: (signal: AbortSignal) => Promise<CodeExerciseResolvedAnswer>
|
|
487
|
+
): Promise<CodeExerciseResolvedAnswer>;
|
|
488
|
+
async function presentThroughBroker(
|
|
489
|
+
broker: InteractionBroker,
|
|
490
|
+
interaction: LearningInteraction,
|
|
491
|
+
signal: AbortSignal | undefined,
|
|
492
|
+
collect: (signal: AbortSignal) => Promise<ResolvedAnswer>
|
|
493
|
+
): Promise<ResolvedAnswer> {
|
|
494
|
+
const localAbort = new AbortController();
|
|
495
|
+
const onToolAbort = () => localAbort.abort();
|
|
496
|
+
if (signal?.aborted) {
|
|
497
|
+
localAbort.abort();
|
|
498
|
+
} else {
|
|
499
|
+
signal?.addEventListener("abort", onToolAbort, { once: true });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const pending = broker.present(interaction, localAbort.signal);
|
|
503
|
+
void pending.catch(() => {
|
|
504
|
+
localAbort.abort();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const outcome = await Promise.race([
|
|
509
|
+
collect(localAbort.signal).then((answer) => ({
|
|
510
|
+
source: "tui" as const,
|
|
511
|
+
answer
|
|
512
|
+
})),
|
|
513
|
+
pending.then((answer) => ({ source: "broker" as const, answer }))
|
|
514
|
+
]);
|
|
515
|
+
if (outcome.source === "broker") {
|
|
516
|
+
localAbort.abort();
|
|
517
|
+
return outcome.answer;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// The TUI resolved the interaction: feed the result back through the
|
|
521
|
+
// broker so the web client sees the same resolved/cancelled events. A
|
|
522
|
+
// skipped TUI answer goes through broker.skip, not submit.
|
|
523
|
+
const submission = "skipped" in outcome.answer
|
|
524
|
+
? broker.skip(interaction.id)
|
|
525
|
+
: broker.submit({
|
|
526
|
+
interactionId: interaction.id,
|
|
527
|
+
answer: outcome.answer.answer,
|
|
528
|
+
clientTimestamp: Date.now()
|
|
529
|
+
});
|
|
530
|
+
if (!submission.ok) {
|
|
531
|
+
throw new Error(submission.message);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return await pending;
|
|
535
|
+
} catch (error) {
|
|
536
|
+
broker.cancel(interaction.id, "tui_interaction_failed");
|
|
537
|
+
await pending.catch(() => undefined);
|
|
538
|
+
throw error;
|
|
539
|
+
} finally {
|
|
540
|
+
signal?.removeEventListener("abort", onToolAbort);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function requireUi(ctx: ExtensionContext, activity: string): void {
|
|
545
|
+
if (!ctx.hasUI) {
|
|
546
|
+
throw new Error(`${activity} requires an interactive Pi UI.`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function presentMultilineEditor(
|
|
551
|
+
ctx: ExtensionContext,
|
|
552
|
+
title: string,
|
|
553
|
+
prefill: string,
|
|
554
|
+
signal: AbortSignal | undefined
|
|
555
|
+
): Promise<string | undefined> {
|
|
556
|
+
if (ctx.mode !== "tui") {
|
|
557
|
+
return ctx.ui.editor(title, prefill);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
ctx.ui.notify(title, "info");
|
|
561
|
+
return ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
562
|
+
const editor = new CustomEditor(
|
|
563
|
+
tui,
|
|
564
|
+
{
|
|
565
|
+
borderColor: (text) => theme.fg("accent", text),
|
|
566
|
+
selectList: getSelectListTheme()
|
|
567
|
+
},
|
|
568
|
+
keybindings,
|
|
569
|
+
{ paddingX: 1 }
|
|
570
|
+
) as CustomEditor & { dispose?: () => void };
|
|
571
|
+
editor.setText(prefill);
|
|
572
|
+
|
|
573
|
+
let settled = false;
|
|
574
|
+
const finish = (value: string | undefined) => {
|
|
575
|
+
if (settled) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
settled = true;
|
|
579
|
+
signal?.removeEventListener("abort", onAbort);
|
|
580
|
+
done(value);
|
|
581
|
+
};
|
|
582
|
+
const onAbort = () => finish(undefined);
|
|
583
|
+
|
|
584
|
+
editor.onSubmit = (text) => finish(text);
|
|
585
|
+
editor.onEscape = () => finish(undefined);
|
|
586
|
+
editor.dispose = () => {
|
|
587
|
+
signal?.removeEventListener("abort", onAbort);
|
|
588
|
+
};
|
|
589
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
590
|
+
if (signal?.aborted) {
|
|
591
|
+
queueMicrotask(onAbort);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return editor;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function throwIfAborted(
|
|
599
|
+
interactionId: string,
|
|
600
|
+
signal: AbortSignal | undefined
|
|
601
|
+
): void {
|
|
602
|
+
if (signal?.aborted) {
|
|
603
|
+
throw new Error(`Learning interaction ${interactionId} was aborted.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function elapsedSince(createdAt: number, now: () => number): number {
|
|
608
|
+
return Math.max(0, now() - createdAt);
|
|
609
|
+
}
|