-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapp.js
More file actions
2102 lines (1982 loc) · 77.7 KB
/
Copy pathapp.js
File metadata and controls
2102 lines (1982 loc) · 77.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Learn Pi — progressive harness tutorial (Obsidian vault UI)
* Structure inspired by shareAI-lab/learn-claude-code
*/
(() => {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const PROGRESS_KEY = "learn-pi-progress";
const THEME_KEY = "learn-pi-theme";
const SIDEBAR_WIDTH_KEY = "learn-pi-sidebar-width";
const SIDEBAR_COLLAPSED_KEY = "learn-pi-sidebar-collapsed";
/** Site base for GitHub project pages (meta[name=site-base], e.g. "/repo/"). */
function siteBase() {
const raw = document.querySelector('meta[name="site-base"]')?.getAttribute("content")?.trim() || "";
if (!raw || raw === "/") return "";
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
}
/** Resolve asset path relative to site root (works on GH Pages root or subpath). */
function assetUrl(path) {
if (!path) return "";
if (/^(https?:|data:|blob:)/i.test(path)) return path;
const clean = path.replace(/^\.\//, "").replace(/^\//, "");
const base = siteBase();
return base ? `${base}/${clean}` : clean;
}
const isApple = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || "") ||
(navigator.userAgentData?.platform === "macOS");
const modKeyLabel = isApple ? "⌘" : "Ctrl";
/**
* @typedef {{ src: string, alt: string, caption?: string, step?: number, width?: number, height?: number }} LessonFigure
* @typedef {{ id: string, sid: string, title: string, motto: string, explain: string, stage: number, concepts: string[], figures?: LessonFigure[], html: string }} Lesson
*/
/** @type {Lesson[]} */
const LESSONS = [
{
id: "s01",
sid: "s01",
title: "Agent Loop",
motto: "One loop is all you need",
explain: "模型决定何时调工具、何时停;harness 只负责执行与回填结果。",
stage: 1,
concepts: ["messages[]", "while True", "tool_use", "tool_result"],
// Image tutorials: add files under assets/lessons/sXX/ — see assets/lessons/README.md
figures: [
{
src: "images/s01-agent-loop.png",
alt: "Xiaohong demonstrates the agent loop from model decision to tool result and return",
caption: "模型决定调用工具或返回文本;harness 执行工具并把结果写回循环。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">一切 Agent 产品的骨架都是同一个循环。Pi 的 <code>pi-agent-core</code> 把循环做在库里;你先理解它,再谈扩展。</p>
<div class="pattern"><span class="hi">User</span> → messages[] → <span class="hi">LLM</span> → response
│
stop / tool_use?
/ \\
tool text
│ │
execute tools return
append results
loop back ────────→ messages[]</div>
<h2>核心不变量</h2>
<ul class="feature-list">
<li><span class="mark">1</span><span>循环本身几乎不变——变的是 tools、context、permissions。</span></li>
<li><span class="mark">2</span><span>模型拥有 agency;代码不「编排智能」,只提供环境。</span></li>
<li><span class="mark">3</span><span>Pi 默认工具极少:<code>read / write / edit / bash</code>,与「One loop + tools」一致。</span></li>
</ul>
<div class="code-block">
<div class="code-block-header"><span>conceptual loop</span><button type="button" class="icon-btn copy-btn" data-copy="while (true) { const response = await llm(messages, tools); messages.push(response); if (!response.hasToolCalls) break; for (const call of response.toolCalls) { const result = await runTool(call); messages.push(result); } }">Copy</button></div>
<pre><span class="comment">// 伪代码:循环归属 Agent;机制归属 Harness</span>
<span class="cmd">while</span> (true) {
const response = <span class="cmd">await</span> llm(messages, tools);
messages.push(response);
<span class="cmd">if</span> (!response.hasToolCalls) <span class="cmd">break</span>;
<span class="cmd">for</span> (const call of response.toolCalls) {
const result = <span class="cmd">await</span> runTool(call);
messages.push(result);
}
}</pre>
</div>
<div class="callout"><div class="callout-icon">※</div><p>在 Pi 源码中对应 <code>packages/agent</code> 的 agent loop + 事件流;UI 层订阅事件,而不是另写一套「工作流引擎」。</p></div>
<details><summary>深潜:AgentMessage vs LLM Message</summary><div class="inner">
<p>Agent 层可以定义比 LLM 协议更丰富的消息类型。调用模型前,上下文会依次经过 <code>transformContext</code> 处理,并由 <code>convertToLlm</code> 转换为 provider 可接受的标准消息;仅供 UI 或会话管理使用的内容不会进入模型请求。这样,会话树、分支元数据等状态可以保留在 harness 中,而不必侵入 provider 的消息协议。</p>
</div></details>`,
},
{
id: "s02",
sid: "s02",
title: "Tools",
motto: "Adding a tool means adding one handler",
explain: "循环不动;新能力注册进 dispatch map。",
stage: 1,
concepts: ["read", "write", "edit", "bash", "registerTool"],
figures: [
{
src: "images/s02-tools.png",
alt: "Xiaohong routes a tool call to read, write, edit, bash, or a registered extension tool",
caption: "默认四工具共享同一分发入口;registerTool 可以继续增加新的 handler。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">Pi 默认只给模型四只手。其它能力(搜索、浏览器、权限门)用 extension 注册工具,而不是 fork 内核。</p>
<div class="grid-2">
<div class="card"><div class="card-title">read</div><p class="card-desc">读文件内容,建立感知。</p></div>
<div class="card"><div class="card-title">write</div><p class="card-desc">创建 / 覆盖文件。</p></div>
<div class="card"><div class="card-title">edit</div><p class="card-desc">精确补丁,少破坏上下文。</p></div>
<div class="card"><div class="card-title">bash</div><p class="card-desc">在项目环境执行命令(Windows 需 Git Bash)。</p></div>
</div>
<h2>扩展工具</h2>
<div class="code-block">
<div class="code-block-header"><span>~/.pi/agent/extensions/greet.ts</span><button type="button" class="icon-btn copy-btn" data-copy="import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; export default function (pi: ExtensionAPI) { pi.registerTool({ name: "greet", description: "Greet someone", parameters: Type.Object({ name: Type.String() }), async execute(_id, params) { return { content: [{ type: "text", text: \`Hello, \${params.name}!\` }], details: {} }; }, }); }">Copy</button></div>
<pre><span class="cmd">export default function</span> (pi: ExtensionAPI) {
pi.registerTool({
name: <span class="cmd">"greet"</span>,
description: <span class="cmd">"Greet someone"</span>,
parameters: Type.Object({ name: Type.String() }),
<span class="cmd">async</span> execute(_id, params) {
<span class="cmd">return</span> { content: [{ type: <span class="cmd">"text"</span>, text: \`Hello, \${params.name}!\` }], details: {} };
},
});
}</pre>
</div>
<div class="callout"><div class="callout-icon">→</div><p>交互里也可用 <code>!command</code> 把 shell 输出注入上下文;<code>!!command</code> 只执行不进上下文。</p></div>`,
},
{
id: "s03",
sid: "s03",
title: "Install & Auth",
motto: "Ship the vehicle before tuning the engine",
explain: "先能跑起来:Node、bash、登录、进项目目录。",
stage: 1,
concepts: ["/login", "auth.json", "Git Bash", "fnm"],
figures: [
{
src: "images/s03-install-auth.png",
alt: "Xiaohong follows the setup path from Node 22 and Git Bash to login and the first prompt",
caption: "Windows 首次运行路径:准备 Node 与 Git Bash,安装 Pi,登录后发送第一条提示。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">Windows + fnm 最短路径如下。Pi 需要 Node ≥ 22.19 与 bash。</p>
<div class="code-block">
<div class="code-block-header"><span>install</span><button type="button" class="icon-btn copy-btn" data-copy="fnm install 22 fnm use 22 npm install -g --ignore-scripts @earendil-works/pi-coding-agent cd your-project pi">Copy</button></div>
<pre><span class="cmd">fnm install 22</span> && <span class="cmd">fnm use 22</span>
<span class="cmd">npm install -g --ignore-scripts @earendil-works/pi-coding-agent</span>
<span class="cmd">cd</span> your-project
<span class="cmd">pi</span></pre>
</div>
<h2>认证</h2>
<ul class="feature-list">
<li><span class="mark">A</span><span><code>/login</code> 订阅 OAuth(Claude / Codex / Copilot…)</span></li>
<li><span class="mark">B</span><span>API Key → 写入 <code>~/.pi/agent/auth.json</code> 或环境变量</span></li>
<li><span class="mark">C</span><span>自定义中转 → 优先 <code>models.json</code>(下一课),不必改 auth.json</span></li>
</ul>
<div class="callout warn"><div class="callout-icon">!</div><p>Windows 请安装 <strong>Git for Windows</strong>,否则 <code>bash</code> 工具不可用。可在 settings 里设 <code>shellPath</code>。</p></div>
<div class="table-wrap"><table>
<thead><tr><th>路径</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>~/.pi/agent/auth.json</code></td><td>内置 provider 凭据</td></tr>
<tr><td><code>~/.pi/agent/settings.json</code></td><td>主题、默认模型、trust 策略</td></tr>
<tr><td><code>~/.pi/agent/models.json</code></td><td>自定义 URL / model</td></tr>
</tbody></table></div>`,
},
{
id: "s04",
sid: "s04",
title: "Models & Providers",
motto: "Point the harness at any capable model",
explain: "URL + Key + Model + API 协议,写进 models.json。",
stage: 1,
concepts: ["models.json", "baseUrl", "openai-responses", "openai-completions"],
figures: [
{
src: "images/s04-models-providers.png",
alt: "Xiaohong connects URL, key, model, and API protocol fields through models.json",
caption: "自定义模型连接由 URL、Key、Model 与 API 协议共同决定。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">内置 catalog 覆盖主流厂商;中转 / 本地 / 私有部署用 <code>~/.pi/agent/models.json</code>。</p>
<div class="code-block">
<div class="code-block-header"><span>models.json · responses 协议示例</span><button type="button" class="icon-btn copy-btn" data-copy='{\n "providers": {\n "my-proxy": {\n "baseUrl": "https://api.example.com/v1",\n "api": "openai-responses",\n "apiKey": "$MY_API_KEY",\n "models": [\n {\n "id": "grok-4.5",\n "name": "Grok 4.5",\n "reasoning": true,\n "input": ["text", "image"],\n "contextWindow": 500000,\n "maxTokens": 500000\n }\n ]\n }\n }\n}'>Copy</button></div>
<pre>{
<span class="cmd">"providers"</span>: {
<span class="cmd">"my-proxy"</span>: {
<span class="cmd">"baseUrl"</span>: <span class="cmd">"https://api.example.com/v1"</span>,
<span class="cmd">"api"</span>: <span class="cmd">"openai-responses"</span>,
<span class="cmd">"apiKey"</span>: <span class="cmd">"$MY_API_KEY"</span>,
<span class="cmd">"models"</span>: [{ <span class="cmd">"id"</span>: <span class="cmd">"grok-4.5"</span>, <span class="cmd">"reasoning"</span>: true, ... }]
}
}
}</pre>
</div>
<ul class="feature-list">
<li><span class="mark">·</span><span><code>api</code>:<code>openai-completions</code> | <code>openai-responses</code> | <code>anthropic-messages</code> | <code>google-generative-ai</code></span></li>
<li><span class="mark">·</span><span>打开 <code>/model</code> 会重载文件;可用 <code>settings.json</code> 设 <code>defaultProvider</code> / <code>defaultModel</code></span></li>
<li><span class="mark">·</span><span>自定义 provider 的 key 在 <code>models.json</code> 即可;<strong>不必</strong>为中转改 <code>auth.json</code></span></li>
</ul>
<div class="callout"><div class="callout-icon">※</div><p>使用 <code>openai-responses</code> 还是 <code>openai-completions</code>,由上游 API 实际支持的协议决定。</p></div>`,
},
{
id: "s05",
sid: "s05",
title: "Sessions",
motto: "History is a tree, not a tape",
explain: "JSONL 会话可分支、恢复、导出——长任务的时间机器。",
stage: 2,
concepts: ["/tree", "/fork", "pi -c", "pi -r", "JSONL"],
figures: [
{
src: "images/s05-sessions.png",
alt: "Xiaohong stands beside a JSONL session tree with continue and fork paths",
caption: "会话历史是一棵树:可以继续当前路径,也可以从节点恢复或 fork。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">每次对话落盘。你可以在树里回到任意节点,fork 出新分支,而不是只能线性 undo。</p>
<div class="grid-2">
<div class="card"><div class="card-title">Continue</div><p class="card-desc"><code>pi -c</code> 继续最近会话</p></div>
<div class="card"><div class="card-title">Resume</div><p class="card-desc"><code>pi -r</code> 浏览历史会话</p></div>
<div class="card"><div class="card-title">Tree</div><p class="card-desc">会话内 <code>/tree</code> 看分支结构</p></div>
<div class="card"><div class="card-title">Fork / Clone</div><p class="card-desc"><code>/fork</code> <code>/clone</code> 分叉实验</p></div>
</div>
<ul class="feature-list">
<li><span class="mark">·</span><span>会话存在 <code>~/.pi/agent/sessions/</code>(按项目隔离)</span></li>
<li><span class="mark">·</span><span>可导出 HTML / JSONL / gist,便于分享 OSS 轨迹</span></li>
<li><span class="mark">·</span><span>双 Esc 默认打开 tree(可在 settings 改)</span></li>
</ul>`,
},
{
id: "s06",
sid: "s06",
title: "Context Files",
motto: "Load knowledge on demand, not as a novel",
explain: "AGENTS.md 是给模型的项目说明书——短、可执行、可继承。",
stage: 2,
concepts: ["AGENTS.md", "CLAUDE.md", "/reload", "~/.pi/agent/AGENTS.md"],
figures: [
{
src: "images/s06-context-files.png",
alt: "Xiaohong merges global, parent, and project context files into the agent loop",
caption: "全局、父目录与当前项目的上下文文件在启动时合并进入 Agent Loop。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">Pi 启动时加载上下文文件,而不是把整个 wiki 塞进 system prompt。</p>
<div class="code-block">
<div class="code-block-header"><span>AGENTS.md</span><button type="button" class="icon-btn copy-btn" data-copy="# Project Instructions - Run npm run check after code changes - Do not run production migrations - Keep replies concise">Copy</button></div>
<pre><span class="comment"># Project Instructions</span>
- Run <span class="cmd">npm run check</span> after code changes
- Do not run production migrations
- Keep replies concise</pre>
</div>
<ul class="feature-list">
<li><span class="mark">·</span><span>全局:<code>~/.pi/agent/AGENTS.md</code></span></li>
<li><span class="mark">·</span><span>项目:当前与父目录的 <code>AGENTS.md</code> / <code>CLAUDE.md</code></span></li>
<li><span class="mark">·</span><span>改完 <code>/reload</code> 或重启 pi</span></li>
</ul>
<div class="quote">你在写的是 <strong>harness 知识层</strong>,不是在训练模型。写清楚边界与命令,比写长篇「角色扮演」有用。</div>`,
},
{
id: "s07",
sid: "s07",
title: "TUI & Themes",
motto: "The vehicle needs a dashboard, not a wallpaper app",
explain: "Pi 是终端 TUI:可换配色主题,壁纸属于终端模拟器。",
stage: 2,
concepts: ["/settings", "theme", "pi-tui", "dark/light"],
figures: [
{
src: "images/s07-tui-themes.png",
alt: "Xiaohong compares the terminal background layer with Pi TUI theme colors",
caption: "终端负责背景;Pi theme 负责 TUI 的文字、边框与组件配色。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">交互界面由 <code>pi-tui</code> 差分渲染。主题是 JSON 色板,不是桌面换肤。</p>
<ul class="feature-list">
<li><span class="mark">·</span><span><code>/settings</code> 切换 theme;内建 <code>dark</code> / <code>light</code></span></li>
<li><span class="mark">·</span><span>自定义:<code>~/.pi/agent/themes/*.json</code></span></li>
<li><span class="mark">·</span><span>改主题文件可热更新;壁纸请在 Windows Terminal 配置</span></li>
</ul>
<div class="callout"><div class="callout-icon">※</div><p>与 Codex 桌面 + Dream Skin 不同:Pi 没有整窗 GUI 背景层。要氛围感 → 终端背景 + Pi theme 配色配合。</p></div>`,
},
{
id: "s08",
sid: "s08",
title: "Extensions",
motto: "Hook around the loop, never rewrite the loop",
explain: "事件、工具、命令、自定义 UI——扩展面就是 Pi 的产品差异。",
stage: 3,
concepts: ["ExtensionAPI", "pi.on", "registerCommand", "/reload", "-e"],
figures: [
{
src: "images/s08-extensions.png",
alt: "Xiaohong attaches extension hooks around an unchanged agent loop",
caption: "Extension 在循环外围监听和拦截事件,不需要重写核心 loop。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">文档原话:Ask pi to build an extension for your use case。无需改源码。</p>
<div class="table-wrap"><table>
<thead><tr><th>位置</th><th>范围</th></tr></thead>
<tbody>
<tr><td><code>~/.pi/agent/extensions/</code></td><td>全局</td></tr>
<tr><td><code>.pi/extensions/</code></td><td>项目(需 trust)</td></tr>
<tr><td><code>pi -e ./x.ts</code></td><td>单次试验</td></tr>
</tbody></table></div>
<h2>能做什么</h2>
<ul class="feature-list">
<li><span class="mark">✓</span><span>生命周期事件:拦截危险 <code>bash</code>、自定义 compaction</span></li>
<li><span class="mark">✓</span><span><code>registerTool</code> / <code>registerCommand</code></span></li>
<li><span class="mark">✓</span><span><code>ctx.ui</code> 确认框、选择器、自定义 TUI 组件</span></li>
<li><span class="mark">✓</span><span>权限门、git checkpoint、todo、甚至游戏——examples 里都有</span></li>
</ul>
<div class="callout warn"><div class="callout-icon">!</div><p>扩展以你的系统权限运行。只装信任来源;项目扩展在 trust 之后才加载。</p></div>`,
},
{
id: "s09",
sid: "s09",
title: "Skills & Templates",
motto: "List first, expand when needed",
explain: "Skills 按需注入知识;Prompt templates 是可复用的斜杠提示。",
stage: 3,
concepts: ["skills", "prompt templates", "/skill:name", "Agent Skills"],
figures: [
{
src: "images/s09-skills-templates.png",
alt: "Xiaohong expands one selected skill and injects it into context on demand",
caption: "先暴露技能清单,需要时再展开正文并注入上下文。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">和把整本手册塞进 system prompt 相反,采用渐进式披露策略:先暴露清单,模型真正需要时再展开正文。</p>
<ul class="feature-list">
<li><span class="mark">·</span><span>Skills:领域流程 / 检查清单,可作 <code>/skill:name</code></span></li>
<li><span class="mark">·</span><span>Prompt templates:固定开场白、审查模板等</span></li>
<li><span class="mark">·</span><span>可放全局、项目或 pi package 分发</span></li>
</ul>
<div class="quote">Pi 刻意不内建 plan mode / todos——你可以用 skill、extension 或 package 按自己的方式实现。</div>`,
},
{
id: "s10",
sid: "s10",
title: "Packages",
motto: "Share the vehicle parts, not the whole factory",
explain: "把 extensions / skills / themes 打成 npm 或 git 包,别人一行安装。",
stage: 3,
concepts: ["pi install", "pi update --extensions", "pi list", "pi config"],
figures: [
{
src: "images/s10-packages.png",
alt: "Xiaohong sends npm, git, and local packages through resource discovery into a Pi session",
caption: "Package 来源经过资源发现与 settings 配置后,加载到 Pi 会话。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<div class="code-block">
<div class="code-block-header"><span>package management</span><button type="button" class="icon-btn copy-btn" data-copy="pi install npm:@foo/bar pi install git:github.com/user/repo pi list pi update --extensions pi config">Copy</button></div>
<pre><span class="cmd">pi install npm:@foo/bar</span>
<span class="cmd">pi install git:github.com/user/repo</span>
<span class="cmd">pi list</span>
<span class="cmd">pi update --extensions</span>
<span class="cmd">pi config</span> <span class="comment"># enable/disable resources</span></pre>
</div>
<ul class="feature-list">
<li><span class="mark">·</span><span>默认写入用户 settings;<code>-l</code> 写项目 <code>.pi/settings.json</code></span></li>
<li><span class="mark">·</span><span><code>pi -e npm:@foo/bar</code> 临时试用,不永久安装</span></li>
<li><span class="mark">·</span><span>包可含 extensions、skills、prompts、themes</span></li>
</ul>`,
},
{
id: "s11",
sid: "s11",
title: "Run Modes",
motto: "Same core, four doors",
explain: "交互、print、JSON、RPC/SDK——同一 harness,不同宿主。",
stage: 4,
concepts: ["interactive", "-p", "--mode json", "--mode rpc", "SDK"],
figures: [
{
src: "images/s11-run-modes.png",
alt: "Xiaohong shows interactive, print, JSON, and RPC or SDK doors around one Pi core",
caption: "运行入口不同,但 Interactive、Print、JSON、RPC 与 SDK 共享同一个核心。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<div class="grid-2">
<div class="card"><div class="card-title">Interactive</div><p class="card-desc">默认 TUI。日常编码。</p><div class="card-meta">pi</div></div>
<div class="card"><div class="card-title">Print</div><p class="card-desc">单次任务,stdout 文本。</p><div class="card-meta">pi -p "…"</div></div>
<div class="card"><div class="card-title">JSON</div><p class="card-desc">结构化事件流,便于管道。</p><div class="card-meta">pi --mode json</div></div>
<div class="card"><div class="card-title">RPC / SDK</div><p class="card-desc">进程集成或嵌入应用(如 OpenClaw)。</p><div class="card-meta">--mode rpc · createAgentSession</div></div>
</div>
<div class="callout"><div class="callout-icon">→</div><p>库拆分:只要 LLM 层用 <code>pi-ai</code>;只要循环用 <code>pi-agent-core</code>;完整产品用 <code>pi-coding-agent</code>。</p></div>`,
},
{
id: "s12",
sid: "s12",
title: "Architecture",
motto: "Libraries first, product on top",
explain: "Monorepo 分层:ai → agent-core → coding-agent + tui。",
stage: 4,
concepts: ["pi-ai", "pi-agent-core", "pi-tui", "pi-coding-agent", "orchestrator"],
figures: [
{
src: "images/s12-architecture.png",
alt: "Xiaohong assembles Pi AI, agent core, TUI, coding agent, and orchestrator layers",
caption: "库层从 pi-ai 与 agent-core 向上组合,coding-agent 位于产品层。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<div class="pattern"><span class="hi">pi-orchestrator</span> (experimental)
│
<span class="hi">pi-coding-agent</span> ← product: CLI / SDK / RPC
┌────┴────┐
<span class="hi">agent-core</span> <span class="hi">pi-tui</span>
│
<span class="hi">pi-ai</span> ← providers, stream, auth</div>
<div class="table-wrap"><table>
<thead><tr><th>Package</th><th>Role</th></tr></thead>
<tbody>
<tr><td><code>@earendil-works/pi-ai</code></td><td>多 provider LLM API</td></tr>
<tr><td><code>@earendil-works/pi-agent-core</code></td><td>循环、状态、harness</td></tr>
<tr><td><code>@earendil-works/pi-tui</code></td><td>差分渲染 TUI</td></tr>
<tr><td><code>@earendil-works/pi-coding-agent</code></td><td>产品 <code>pi</code></td></tr>
<tr><td><code>@earendil-works/pi-orchestrator</code></td><td>实验性编排</td></tr>
</tbody></table></div>
<p>版本 lockstep;文档:<a href="https://pi.dev/docs/latest" target="_blank" rel="noopener">pi.dev/docs</a>。</p>`,
},
{
id: "s13",
sid: "s13",
title: "Compaction",
motto: "Context always fills up — make room",
explain: "长会话靠 compaction;完整历史仍在 JSONL,可用 /tree 找回。",
stage: 5,
concepts: ["/compact", "auto-compaction", "lossy summary"],
figures: [
{
src: "images/s13-compaction.png",
alt: "Xiaohong compresses a full context stack into a summary while preserving full history",
caption: "Compaction 用摘要腾出上下文空间;完整历史仍保存在会话记录中。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">Context 总会满。Pi 提供手动与自动 compaction;策略可用 extension 定制。</p>
<ul class="feature-list">
<li><span class="mark">·</span><span>压缩有损:摘要替换旧轮次,细节可能丢失</span></li>
<li><span class="mark">·</span><span>原始轨迹仍在会话文件;<code>/tree</code> 可回看节点</span></li>
<li><span class="mark">·</span><span>扩展可拦截 compact 流程,换模型总结或注入规则</span></li>
</ul>
<div class="quote">无限会话不是无限注意力——harness 的职责是<strong>腾地方</strong>,不是假装上下文无限。</div>`,
},
{
id: "s14",
sid: "s14",
title: "Trust & Safety",
motto: "Set boundaries first, then grant freedom",
explain: "Pi 默认无内建权限弹窗;信任项目 + 沙箱/扩展 = 边界。",
stage: 5,
concepts: ["project trust", "trust.json", "containerization", "defaultProjectTrust"],
figures: [
{
src: "images/s14-trust-safety.png",
alt: "Xiaohong controls project trust, extension loading, skipping, and container isolation",
caption: "项目资源在信任后加载;高风险执行应使用容器等外部隔离边界。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">设计选择:不把 permission UX 写死在核心。边界来自你的环境与扩展。</p>
<ul class="feature-list">
<li><span class="mark">·</span><span>含 <code>.pi/</code> 的项目会问 trust;决定写入 <code>trust.json</code></span></li>
<li><span class="mark">·</span><span>不信任则不加载项目扩展与项目 settings</span></li>
<li><span class="mark">·</span><span>强隔离:Docker / Gondolin / OpenShell(见 containerization 文档)</span></li>
<li><span class="mark">·</span><span>危险命令确认:自己写 extension 拦 <code>tool_call</code></span></li>
</ul>
<div class="callout warn"><div class="callout-icon">!</div><p>默认以当前用户权限运行。生产或不可信仓库请容器化,不要假设「agent 自己有权限系统」。</p></div>`,
},
{
id: "s15",
sid: "s15",
title: "Philosophy",
motto: "Adapt pi to your workflow — don't fork",
explain: "最小核心 + 扩展面 = harness 工程。模型是司机,Pi 是车。",
stage: 6,
concepts: ["minimal core", "no fork", "harness engineering", "OSS sessions"],
figures: [
{
src: "images/s15-philosophy.png",
alt: "Xiaohong adds extension, skill, and package modules to a Pi car instead of forking it",
caption: "模型负责驾驶;通过 extensions、skills 与 packages 改装 Pi,而不是 fork 核心。",
step: 1,
width: 1168,
height: 784,
},
],
html: `
<p class="lead">学完前面 14 课,回到产品哲学——这也是 Pi 与「全能 IDE Agent」的分水岭。</p>
<div class="grid-2">
<div class="card"><div class="card-title">刻意不做</div><p class="card-desc">内建 MCP、sub-agents、plan mode、todos、权限弹窗、background bash… 交给扩展或沙箱。</p></div>
<div class="card"><div class="card-title">刻意做好</div><p class="card-desc">循环、工具、会话树、多 provider、TUI、扩展 API、SDK/RPC、包分发。</p></div>
</div>
<div class="quote"><strong>Agency comes from the model. The harness gives agency a place to land.</strong><br/>Build the harness well. The model will do the rest.</div>
<ul class="feature-list">
<li><span class="mark">1</span><span>用 <code>models.json</code> 接任意模型</span></li>
<li><span class="mark">2</span><span>用 extensions / skills / packages 塑形工作流</span></li>
<li><span class="mark">3</span><span>用 sessions 积累真实轨迹;可选择公开 OSS sessions 反哺生态</span></li>
</ul>
<div class="link-strip">
<a class="btn primary" href="https://pi.dev/docs/latest" target="_blank" rel="noopener">pi.dev docs</a>
<a class="btn" href="https://github.com/earendil-works/pi" target="_blank" rel="noopener">GitHub</a>
<a class="btn" href="https://github.com/shareAI-lab/learn-claude-code" target="_blank" rel="noopener">learn-claude-code(姊妹教程思路)</a>
</div>`,
},
];
/**
* Mermaid sources — labels with special chars MUST use double quotes:
* A["text with / ? : ~ +"] and D{"question?"}
*/
const MERMAIDS = {
s01: {
title: "流程图 · Agent Loop",
src: `flowchart TD
A["User prompt"] --> B["Append to messages"]
B --> C["Call LLM with tools"]
C --> D{"Has tool_use"}
D -->|yes| E["Execute tools"]
E --> F["Append tool_result"]
F --> C
D -->|no| G["Return text"]
G --> H["End turn"]`,
},
s02: {
title: "流程图 · Tool dispatch",
src: `flowchart LR
L["LLM"] -->|tool_call| D["Dispatch map"]
D --> R["read"]
D --> W["write"]
D --> E["edit"]
D --> B["bash"]
D --> X["extension tools"]
R --> TR["tool_result"]
W --> TR
E --> TR
B --> TR
X --> TR
TR --> L`,
},
s03: {
title: "流程图 · First run",
src: `flowchart TD
A["Install Node 22+"] --> B["npm install pi-coding-agent"]
B --> C["Git Bash on Windows"]
C --> D["cd project"]
D --> E["Run pi"]
E --> F["login command"]
F --> G["OAuth subscription"]
F --> H["API key"]
G --> I["model picker"]
H --> I
I --> J["First prompt"]`,
},
s04: {
title: "流程图 · Model resolution",
src: `flowchart TD
A["Request model"] --> B{"Source"}
B -->|"built-in"| C["Provider catalog"]
B -->|"custom"| D["models.json"]
D --> E["baseUrl + api + models"]
E --> F["apiKey env or auth"]
C --> F
F --> G{"Auth ok"}
G -->|yes| H["Available in model list"]
G -->|no| I["Listed but unavailable"]
H --> J["Stream via API protocol"]`,
},
s05: {
title: "流程图 · Session tree",
src: `flowchart TD
A["pi start"] --> B["Load or create session JSONL"]
B --> C["Interactive turns"]
C --> D["Persist entries"]
D --> E["tree fork clone"]
E --> F["Branch node"]
F --> C
C --> G["continue or resume"]
G --> B`,
},
s06: {
title: "流程图 · Context loading",
src: `flowchart TD
A["pi startup"] --> B["Global AGENTS.md"]
A --> C["Parent dir context files"]
A --> D["Cwd context files"]
B --> E["Merge context"]
C --> E
D --> E
E --> F["System and agent context"]
F --> G["Agent loop"]
H["reload command"] --> A`,
},
s07: {
title: "流程图 · Theme layers",
src: `flowchart TB
subgraph TERM["Terminal"]
TW["Terminal wallpaper colors"]
end
subgraph PITUI["Pi TUI"]
TH["theme JSON"]
UI["Components borders text"]
end
TW -.->|background| UI
TH --> UI
S["settings command"] --> TH`,
},
s08: {
title: "流程图 · Extension hooks",
src: `flowchart TD
A["Session start"] --> B["Load extensions"]
B --> C["Agent loop"]
C --> D["tool_call event"]
D --> E{"Extension handler"}
E -->|block| F["Deny with reason"]
E -->|allow| G["Execute tool"]
G --> H["tool_result"]
H --> C
B --> I["registerTool registerCommand"]
I --> C`,
},
s09: {
title: "流程图 · On-demand skills",
src: `flowchart LR
A["Skill manifests list"] --> B["Model sees names"]
B --> C{"Need skill"}
C -->|yes| D["Expand skill body"]
D --> E["Inject into context"]
E --> F["Continue loop"]
C -->|no| F
T["Prompt template"] --> E`,
},
s10: {
title: "流程图 · Package install",
src: `flowchart TD
A["pi install source"] --> B{"Source type"}
B -->|npm| C["user agent npm dir"]
B -->|git| D["user agent git dir"]
B -->|path| E["Local path"]
C --> F["Discover resources"]
D --> F
E --> F
F --> G["Write settings.json"]
G --> H["Load in session"]
I["temp install flag"] --> F`,
},
s11: {
title: "流程图 · Run modes",
src: `flowchart TB
CORE["pi-agent-core and pi-ai"]
CORE --> MI["Interactive TUI"]
CORE --> MP["Print mode"]
CORE --> MJ["JSON mode"]
CORE --> MR["RPC mode"]
CORE --> MS["SDK embed"]
MI --> USER["Human terminal"]
MP --> CI["Scripts CI"]
MJ --> PIPE["Pipelines"]
MR --> HOST["Other process"]
MS --> APP["Your app"]`,
},
s12: {
title: "流程图 · Package layers",
src: `flowchart BT
AI["pi-ai"] --> AC["pi-agent-core"]
AI --> CA["pi-coding-agent"]
AC --> CA
TUI["pi-tui"] --> CA
CA --> OR["pi-orchestrator"]
CA --> CLI["cli bin pi"]`,
},
s13: {
title: "流程图 · Compaction",
src: `flowchart TD
A["Long messages"] --> B{"Context full"}
B -->|no| C["Continue loop"]
B -->|yes| D["Auto or compact"]
D --> E["Summarize old turns"]
E --> F["Replace with summary"]
F --> G["JSONL keeps full history"]
G --> H["tree can revisit"]
F --> C`,
},
s14: {
title: "流程图 · Project trust",
src: `flowchart TD
A["Enter project"] --> B{"Has project config"}
B -->|no| C["User tools only"]
B -->|yes| D{"Trusted already"}
D -->|yes| E["Load project extensions"]
D -->|no| F["Prompt trust"]
F -->|trust| G["Save trust.json"]
G --> E
F -->|deny| H["Skip project resources"]
E --> I["Agent loop"]
H --> I
C --> I
J["Container sandbox"] -.-> I`,
},
s15: {
title: "流程图 · Extend not fork",
src: `flowchart LR
M["Model agency"] --> H["Pi minimal core"]
H --> E["Extensions"]
H --> K["Skills templates"]
H --> P["Packages"]
H --> C["Custom models.json"]
E --> W["Your workflow"]
K --> W
P --> W
C --> W
X["Fork internals"] -.->|avoid| H`,
},
};
const HOME_ID = "home";
const ALL_VIEWS = [{ id: HOME_ID, sid: "", title: "Home", motto: "Overview" }, ...LESSONS];
/**
* Full-text search index: title/meta + stripped lesson body text.
* SEARCH_RAW keeps readable text for palette excerpts.
*/
const SEARCH_RAW = new Map();
const SEARCH_INDEX = new Map();
for (const l of LESSONS) {
const div = document.createElement("div");
div.innerHTML = l.html;
const body = (div.textContent || "").replace(/\s+/g, " ").trim();
const meta = `${l.sid} ${l.title} ${l.motto} ${l.explain} ${l.concepts.join(" ")}`;
SEARCH_RAW.set(l.id, { meta, body });
SEARCH_INDEX.set(l.id, `${meta} ${body}`.toLowerCase());
}
SEARCH_INDEX.set(HOME_ID, "home 首页 overview learn pi 教程 harness");
function loadProgress() {
try {
return new Set(JSON.parse(localStorage.getItem(PROGRESS_KEY) || "[]"));
} catch {
return new Set();
}
}
function saveProgress(set) {
localStorage.setItem(PROGRESS_KEY, JSON.stringify([...set]));
}
let progress = loadProgress();
function nextIncompleteLesson() {
return LESSONS.find((l) => !progress.has(l.id)) || null;
}
function updateContinueCTA() {
const btn = $("#continue-btn");
const hint = $("#continue-hint");
const progressNext = $("#progress-next");
if (!btn) return;
const done = LESSONS.filter((l) => progress.has(l.id)).length;
const next = nextIncompleteLesson();
if (done === 0) {
btn.setAttribute("href", "#s01");
btn.textContent = "Start s01 →";
if (hint) {
hint.hidden = true;
hint.textContent = "";
}
if (progressNext) progressNext.textContent = "Start with s01";
return;
}
if (!next) {
btn.setAttribute("href", "#home");
btn.textContent = "全部完成 · 回 Home";
if (hint) {
hint.hidden = false;
hint.textContent = "15 课已完成。可从侧栏复习任意一课,或阅读官方文档继续深入。";
}
if (progressNext) progressNext.textContent = "All lessons complete";
return;
}
btn.setAttribute("href", `#${next.id}`);
btn.textContent = `Continue ${next.sid} · ${next.title} →`;
if (hint) {
hint.hidden = false;
hint.textContent = `已完成 ${done} / ${LESSONS.length} · 下一课 ${next.sid}`;
}
if (progressNext) progressNext.textContent = `Next: ${next.sid} ${next.title}`;
}
function updateStageProgress() {
const byStage = new Map();
for (const l of LESSONS) {
if (!byStage.has(l.stage)) byStage.set(l.stage, []);
byStage.get(l.stage).push(l);
}
for (const [stage, list] of byStage) {
const done = list.filter((l) => progress.has(l.id)).length;
const el = $(`[data-stage-progress="${stage}"]`);
if (el) {
el.textContent = `${done}/${list.length}`;
el.classList.toggle("complete", done === list.length && list.length > 0);
}
const stageEl = $(`.stage[data-stage="${stage}"]`);
if (stageEl) {
stageEl.classList.toggle("stage-complete", done === list.length && list.length > 0);
// Highlight current stage (first incomplete)
const next = nextIncompleteLesson();
stageEl.classList.toggle("stage-current", Boolean(next && next.stage === stage));
}
}
}
function updateProgressUI() {
const total = LESSONS.length;
const done = LESSONS.filter((l) => progress.has(l.id)).length;
const pct = total ? Math.round((done / total) * 100) : 0;
const fill = $("#progress-fill");
const label = $("#progress-label");
if (fill) fill.style.width = `${pct}%`;
if (label) label.textContent = `${done} / ${total}`;
$$(".nav-item[data-nav]").forEach((el) => {
const id = el.dataset.nav;
el.classList.toggle("done", progress.has(id));
});
$$(".lesson-card").forEach((el) => {
el.classList.toggle("done", progress.has(el.dataset.nav));
});
$$(".done-btn").forEach((btn) => {
const id = btn.dataset.mark;
const isDone = progress.has(id);
btn.classList.toggle("is-done", isDone);
btn.textContent = isDone ? "✓ 已完成" : "标记完成";
});
updateContinueCTA();
updateStageProgress();
}
let toastTimer = 0;
function showToast(message, { actionLabel, onAction } = {}) {
const el = $("#toast");
if (!el) return;
el.hidden = false;
el.replaceChildren();
const text = document.createElement("span");
text.textContent = message;
el.appendChild(text);
if (actionLabel && onAction) {
const action = document.createElement("button");
action.type = "button";
action.className = "toast-action";
action.textContent = actionLabel;
action.addEventListener("click", () => {
onAction();
el.hidden = true;
});
el.appendChild(action);
}
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => {
el.hidden = true;
}, 4200);
}
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
function scrollBehavior() {
return reducedMotion.matches ? "auto" : "smooth";
}
/** Parse "#s08" or "#s08/heading-slug" into view id + in-page anchor suffix. */
function parseHash() {
let raw = location.hash.slice(1);
try {
raw = decodeURIComponent(raw);
} catch {
// Keep the raw hash when a malformed escape sequence is supplied.
}
const [viewPart, anchorPart] = raw.split("/");
const view = ALL_VIEWS.some((v) => v.id === viewPart) ? viewPart : HOME_ID;
return { view, anchor: anchorPart || "" };
}
function scrollToAnchor(viewId, anchor) {
const el = document.getElementById(`${viewId}-${anchor}`);
if (!el) return;
el.scrollIntoView({ behavior: scrollBehavior(), block: "start" });
}
function setView(
id,
{ renderDiagrams = true, anchor = "", manageFocus = true } = {},
) {
const valid = ALL_VIEWS.some((v) => v.id === id) ? id : HOME_ID;
$$(".view").forEach((el) => el.classList.toggle("active", el.dataset.view === valid));
$$(".nav-item").forEach((el) => el.classList.toggle("active", el.dataset.nav === valid));
const meta = ALL_VIEWS.find((v) => v.id === valid);
const crumb = $("#breadcrumb-current");
if (crumb && meta) {
crumb.textContent = meta.sid ? `${meta.sid} · ${meta.title}` : meta.title;
}
setSidebarOpen(false);
updateOutline(valid, anchor);
// Move focus to the destination so keyboard and screen-reader users
// land on the new lesson/section after hash navigation.
const destination = anchor
? document.getElementById(`${valid}-${anchor}`)
: $(`.view[data-view="${valid}"] h1`);
if (manageFocus && destination) {
destination.setAttribute("tabindex", "-1");
destination.focus({ preventScroll: true });
}
// Mermaid cannot reliably measure SVG in display:none views — render only the active one.
if (renderDiagrams) void renderMermaid({ force: false });
if (anchor) {
// Wait for diagrams to paint so the anchor position is stable.
void mermaidRenderQueue.then(() => {
requestAnimationFrame(() => scrollToAnchor(valid, anchor));
});
} else {
window.scrollTo({ top: 0, behavior: scrollBehavior() });
}
}
function navigateTo(view, anchor = "") {
const nextHash = `#${view}${anchor ? `/${encodeURIComponent(anchor)}` : ""}`;
if (location.hash === nextHash) syncFromURL();
else location.hash = nextHash;
}
function syncFromURL({ manageFocus = true } = {}) {
const { view, anchor } = parseHash();
setView(view, { anchor, manageFocus });
}