@linxin666/dsh-pet 0.2.0 → 0.2.2
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/README.i18n.yaml +2 -2
- package/README.md +15 -5
- package/README.zh.md +15 -5
- package/assets/whale/pet.json +1 -1
- package/assets/whale-refined/pet.json +7 -0
- package/assets/whale-refined/spritesheet.webp +0 -0
- package/lib/client.js +257 -53
- package/lib/client.js.map +1 -1
- package/lib/index.js +235 -83
- package/lib/invariant.js +1 -1
- package/lib/{state-Ch3f4luZ.js → state-DrMX22GL.js} +35 -5
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +167 -77
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +1 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +54 -11
- package/lib/types/client/PluginSettingsCard.d.ts +22 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +142 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +1 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/spritesheet.d.ts +1 -3
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +1 -5
- package/lib/types/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- package/lib/types/registry.d.ts +0 -2
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +11 -7
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/remarks.js +30 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +14 -0
- package/lib/types/service.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/chatter.ts +167 -77
- package/src/client/PetDockEntry.test.tsx +93 -0
- package/src/client/PetDockEntry.tsx +1 -0
- package/src/client/PetSprite.test.tsx +72 -11
- package/src/client/PetSprite.tsx +98 -30
- package/src/client/PluginSettingsCard.tsx +219 -18
- package/src/client/index.test.tsx +95 -0
- package/src/client/index.ts +1 -0
- package/src/client/locales.ts +4 -0
- package/src/client/pet-css.test.ts +15 -3
- package/src/client/pet.module.css +101 -71
- package/src/client/settings-card.module.css +90 -1
- package/src/client/spritesheet.ts +1 -6
- package/src/loopback.ts +63 -0
- package/src/registry.test.ts +49 -1
- package/src/registry.ts +11 -7
- package/src/remarks.ts +30 -0
- package/src/routes.ts +11 -0
- package/src/service.ts +0 -1
package/src/chatter.ts
CHANGED
|
@@ -494,46 +494,89 @@ export const WHISPER_TTL_MS = 8000
|
|
|
494
494
|
/** Ambient inner-whisper pool (no keyword needed; earned by output volume). */
|
|
495
495
|
export const WHISPER_GENERIC_POOL: readonly string[] = [
|
|
496
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
|
-
'
|
|
497
|
+
'loading 99%……就差最后一步',
|
|
498
|
+
'嗯……让我捋捋',
|
|
499
|
+
'灵感来了,先记小本本上',
|
|
500
|
+
'脑子在冒烟,但还能撑',
|
|
501
|
+
'这个报错,我好像在哪见过',
|
|
502
|
+
'专注模式 ON,请勿打扰',
|
|
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
|
+
'思考.gif 加载中',
|
|
538
|
+
'我本地能跑啊……哦,我就是干活的',
|
|
539
|
+
'有点饿,小鱼干存货还够吗',
|
|
540
|
+
'刚想通,一被打断又忘了,气',
|
|
541
|
+
'缓冲中,请稍候',
|
|
542
|
+
'这网速,比我思考还慢',
|
|
543
|
+
'脑子在后台跑批',
|
|
544
|
+
'内存不足,但热情够',
|
|
545
|
+
'404:思路未找到,重试中',
|
|
546
|
+
'这需求有点玄学,但能写',
|
|
547
|
+
'诶,这 bug 还会闪现?',
|
|
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
|
+
'你专注的样子,我默默记下了',
|
|
537
580
|
]
|
|
538
581
|
|
|
539
582
|
/** Keyword-triggered whisper rules, most specific moods first. */
|
|
@@ -541,67 +584,97 @@ export const WHISPER_RULES: readonly WhisperRule[] = [
|
|
|
541
584
|
{
|
|
542
585
|
keywords: ['测试通过', '测试全过', '全部通过', 'all tests pass', 'tests passed', 'test passed', '全绿'],
|
|
543
586
|
pool: [
|
|
544
|
-
'
|
|
545
|
-
'
|
|
546
|
-
'
|
|
547
|
-
'
|
|
548
|
-
'
|
|
549
|
-
'
|
|
587
|
+
'全绿!亮瞎我眼了',
|
|
588
|
+
'测试全过,击掌~',
|
|
589
|
+
'稳了稳了,这波稳得很',
|
|
590
|
+
'绿灯一排排,看着就舒坦',
|
|
591
|
+
'能跑!没报错!',
|
|
592
|
+
'全绿,收工摸鱼去',
|
|
593
|
+
'测试过了,尾巴翘上天',
|
|
594
|
+
'漂亮,一次过',
|
|
595
|
+
'测试过了,奖励自己一口小鱼干',
|
|
596
|
+
'绿得发光,稳',
|
|
597
|
+
'又双叒叕全绿',
|
|
598
|
+
'这波测试,赢得干脆',
|
|
550
599
|
],
|
|
551
600
|
},
|
|
552
601
|
{
|
|
553
602
|
keywords: ['错误', '失败', '报错', '异常', '崩溃', 'bug', 'error', 'failed', 'exception', 'traceback', '找不到', '不对了'],
|
|
554
603
|
pool: [
|
|
555
|
-
'
|
|
556
|
-
'
|
|
557
|
-
'
|
|
558
|
-
'
|
|
559
|
-
'
|
|
560
|
-
'
|
|
561
|
-
'
|
|
562
|
-
'
|
|
604
|
+
'哎呀,踩到小石子了',
|
|
605
|
+
'翻车了……没事,扶起来继续',
|
|
606
|
+
'错误是进步的脚印,我懂',
|
|
607
|
+
'这报错我盯上它了',
|
|
608
|
+
'我本地能跑啊?哦,我一直在跑',
|
|
609
|
+
'别慌,深呼吸,先看报错',
|
|
610
|
+
'bug 你站住,我看见你了',
|
|
611
|
+
'绷不住了……好,继续修',
|
|
612
|
+
'报错这东西,见一个修一个',
|
|
613
|
+
'又是它,老熟人了',
|
|
614
|
+
'问题不大,就是有点问题',
|
|
615
|
+
'先别慌,我看看到底咋回事',
|
|
616
|
+
'这错报得,比我还委屈',
|
|
617
|
+
'修好它,今天才不算白干',
|
|
563
618
|
],
|
|
564
619
|
},
|
|
565
620
|
{
|
|
566
621
|
keywords: ['等等', '不对', '重新想', '再想想', '换个思路', '我搞错了', '纠正', '其实应该'],
|
|
567
622
|
pool: [
|
|
568
623
|
'嗯?让我再想想……',
|
|
569
|
-
'
|
|
570
|
-
'
|
|
571
|
-
'发现岔路,及时掉头~',
|
|
572
|
-
'自我纠错的瞬间最帅了',
|
|
624
|
+
'推翻重来,也是种勇气',
|
|
625
|
+
'发现岔路,及时掉头',
|
|
573
626
|
'不对不对,重来重来',
|
|
627
|
+
'自我纠错的瞬间,最帅了',
|
|
628
|
+
'呃,刚说错了,收回',
|
|
629
|
+
'哎,绕远了,拉回来',
|
|
630
|
+
'回头一看,原来这么简单',
|
|
631
|
+
'纠正完,思路清爽多了',
|
|
632
|
+
'转弯不丢人,卡死才丢人',
|
|
574
633
|
],
|
|
575
634
|
},
|
|
576
635
|
{
|
|
577
636
|
keywords: ['首先', '接下来', '第一步', '第二步', '计划', '步骤', 'todo', '任务拆解', '分工'],
|
|
578
637
|
pool: [
|
|
579
638
|
'排排坐,分果果',
|
|
580
|
-
'
|
|
639
|
+
'计划通,执行开始',
|
|
581
640
|
'一步一步来,不慌',
|
|
582
|
-
'
|
|
641
|
+
'大任务切成小块块,好下口',
|
|
583
642
|
'清单列好了,逐个击破',
|
|
584
|
-
'
|
|
643
|
+
'谋定而后动,这节奏我熟',
|
|
644
|
+
'先干这个,再干那个',
|
|
645
|
+
'头绪理清了,开整',
|
|
646
|
+
'步骤在手,心里不慌',
|
|
647
|
+
'安排得明明白白',
|
|
585
648
|
],
|
|
586
649
|
},
|
|
587
650
|
{
|
|
588
651
|
keywords: ['终于', '搞定', '完成了', '解决了', '成功了', '修复了', 'done', 'fixed', 'solved', '完成啦'],
|
|
589
652
|
pool: [
|
|
590
|
-
'
|
|
591
|
-
'
|
|
592
|
-
'
|
|
593
|
-
'
|
|
594
|
-
'
|
|
595
|
-
'
|
|
653
|
+
'太好了,又翻过一页',
|
|
654
|
+
'搞定,收工~',
|
|
655
|
+
'攻下一城,击掌!',
|
|
656
|
+
'难题被拿下了,转个圈',
|
|
657
|
+
'努力没白费,开心',
|
|
658
|
+
'齐活,漂亮',
|
|
659
|
+
'收工收工,今天圆满',
|
|
660
|
+
'完成!心里踏实了',
|
|
661
|
+
'这波,稳得一批',
|
|
662
|
+
'任务清零,舒服',
|
|
663
|
+
'搞定,可以伸个懒腰了',
|
|
664
|
+
'又完成一件,成就感+1',
|
|
596
665
|
],
|
|
597
666
|
},
|
|
598
667
|
{
|
|
599
668
|
keywords: ['谢谢', '感谢', 'thank'],
|
|
600
669
|
pool: [
|
|
601
|
-
'
|
|
670
|
+
'不客气呀,顺手的事',
|
|
602
671
|
'被感谢了,心里甜甜的',
|
|
603
672
|
'能帮上忙就好~',
|
|
604
|
-
'
|
|
673
|
+
'你的谢意,我收进口袋啦',
|
|
674
|
+
'这话我爱听',
|
|
675
|
+
'客气啥,应该的',
|
|
676
|
+
'收下这份心意,干劲+1',
|
|
677
|
+
'你谢我,我谢你,扯平啦',
|
|
605
678
|
],
|
|
606
679
|
},
|
|
607
680
|
{
|
|
@@ -610,18 +683,27 @@ export const WHISPER_RULES: readonly WhisperRule[] = [
|
|
|
610
683
|
'难不倒我们俩的',
|
|
611
684
|
'越难啃的骨头越香',
|
|
612
685
|
'硬骨头?我最喜欢了',
|
|
613
|
-
'
|
|
614
|
-
'
|
|
686
|
+
'复杂问题拆开看,小事',
|
|
687
|
+
'这坑我们一起填',
|
|
688
|
+
'有点东西,但不多',
|
|
689
|
+
'硬骨头,慢慢啃',
|
|
690
|
+
'问题越难,赢的时候越爽',
|
|
691
|
+
'绕是绕不过去的,正面刚',
|
|
692
|
+
'再难的题,拆开都是小问号',
|
|
615
693
|
],
|
|
616
694
|
},
|
|
617
695
|
{
|
|
618
696
|
keywords: ['检查', '审查', '确认一下', '核对', 'review', '仔细看看', '验证'],
|
|
619
697
|
pool: [
|
|
620
|
-
'
|
|
698
|
+
'火眼金睛,启动',
|
|
621
699
|
'让我仔细瞧瞧',
|
|
622
|
-
'
|
|
700
|
+
'细节魔鬼,一个都不放过',
|
|
623
701
|
'认真检查的样子最迷人',
|
|
624
702
|
'多核一遍,稳上加稳',
|
|
703
|
+
'再看一眼,不亏',
|
|
704
|
+
'确认键,点了才安心',
|
|
705
|
+
'细节控上线',
|
|
706
|
+
'查完这遍,稳了',
|
|
625
707
|
],
|
|
626
708
|
},
|
|
627
709
|
{
|
|
@@ -629,17 +711,25 @@ export const WHISPER_RULES: readonly WhisperRule[] = [
|
|
|
629
711
|
pool: [
|
|
630
712
|
'去知识的海洋里捞一捞',
|
|
631
713
|
'翻翻找找,线索快出来',
|
|
632
|
-
'
|
|
633
|
-
'
|
|
714
|
+
'检索小雷达启动',
|
|
715
|
+
'答案就藏在某个角落',
|
|
716
|
+
'线索有点散,拼一下',
|
|
717
|
+
'找东西,我最在行',
|
|
718
|
+
'答案在网线那头等我',
|
|
719
|
+
'翻箱倒柜中,稍等',
|
|
634
720
|
],
|
|
635
721
|
},
|
|
636
722
|
{
|
|
637
723
|
keywords: ['写代码', '实现', '编码', '函数', '接口', '重构'],
|
|
638
724
|
pool: [
|
|
639
725
|
'指尖跳舞,代码开花',
|
|
640
|
-
'
|
|
641
|
-
'写代码的样子像在作画',
|
|
726
|
+
'把逻辑织成网',
|
|
642
727
|
'一行一行,垒起小城堡',
|
|
728
|
+
'这代码写得,我自己都佩服',
|
|
729
|
+
'码着码着,天就亮了',
|
|
730
|
+
'代码跑通了,比中奖还开心',
|
|
731
|
+
'这行代码,写得有点帅',
|
|
732
|
+
'写完再润润,讲究',
|
|
643
733
|
],
|
|
644
734
|
},
|
|
645
735
|
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* The hidden-state summon button opts into the L2 semantic attributes
|
|
4
|
+
* (issue #506): it carries data-dsh-part="summon-button" so skins can target
|
|
5
|
+
* it without hash-class selectors.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
9
|
+
// The npm SDK's client half is a closure-factory bundle for the GUI's
|
|
10
|
+
// __ModuleLoader__ (not importable under vitest); provide the defineStore
|
|
11
|
+
// the pet store needs (same fake-store pattern as the settings-card tests).
|
|
12
|
+
vi.mock('@deepseek-ai/dsh-client-runtime/client', () => ({
|
|
13
|
+
defineStore: (spec: {
|
|
14
|
+
init: () => unknown
|
|
15
|
+
actions: Record<string, (draft: never, ...args: never[]) => void>
|
|
16
|
+
}) => ({
|
|
17
|
+
create: () => {
|
|
18
|
+
let value = spec.init()
|
|
19
|
+
const listeners = new Set<() => void>()
|
|
20
|
+
const actions: Record<string, (...args: unknown[]) => void> = {}
|
|
21
|
+
for (const [name, fn] of Object.entries(spec.actions)) {
|
|
22
|
+
actions[name] = (...args: unknown[]) => {
|
|
23
|
+
fn(value as never, ...(args as never[]))
|
|
24
|
+
for (const listener of listeners) listener()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
getSnapshot: () => value,
|
|
29
|
+
subscribe: (listener: () => void) => {
|
|
30
|
+
listeners.add(listener)
|
|
31
|
+
return () => { listeners.delete(listener) }
|
|
32
|
+
},
|
|
33
|
+
actions,
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
}))
|
|
38
|
+
import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
|
|
39
|
+
import { createPetStore } from './pet-store.ts'
|
|
40
|
+
import { t } from './locales.ts'
|
|
41
|
+
import type { PetStateView } from '../service.ts'
|
|
42
|
+
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
document.documentElement.lang = 'zh'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(cleanup)
|
|
48
|
+
|
|
49
|
+
/** Snapshot fixture with the pet hidden (the summon-button state). */
|
|
50
|
+
const hiddenSnapshot: PetStateView = {
|
|
51
|
+
animation: 'idle',
|
|
52
|
+
phase: 'idle',
|
|
53
|
+
sessionActive: false,
|
|
54
|
+
affinity: {
|
|
55
|
+
points: 0,
|
|
56
|
+
rank: '幼鲸',
|
|
57
|
+
rankEmoji: '*',
|
|
58
|
+
pets: 0,
|
|
59
|
+
feeds: 0,
|
|
60
|
+
turns: 0,
|
|
61
|
+
petCooldown: false,
|
|
62
|
+
feedCooldown: false,
|
|
63
|
+
},
|
|
64
|
+
display: { visible: false, size: 160, right: 24, bottom: 20 },
|
|
65
|
+
pet: { id: 'whale-girl', displayName: '鲸鱼娘', description: '测试用鲸鱼娘' },
|
|
66
|
+
name: '泡泡',
|
|
67
|
+
treats: { stocked: 0, max: 5 },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function injected(): PetInjected {
|
|
71
|
+
return {
|
|
72
|
+
store: createPetStore().create(),
|
|
73
|
+
ensure: vi.fn(),
|
|
74
|
+
pet: vi.fn(),
|
|
75
|
+
feed: vi.fn(),
|
|
76
|
+
hide: vi.fn(),
|
|
77
|
+
summon: vi.fn(),
|
|
78
|
+
dragEnd: vi.fn(),
|
|
79
|
+
rename: vi.fn(),
|
|
80
|
+
openSession: vi.fn(),
|
|
81
|
+
feedbackDone: vi.fn(),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('PetDockEntry L2 semantic attributes (#506)', () => {
|
|
86
|
+
it('tags the summon button as the summon-button part', () => {
|
|
87
|
+
const props = injected()
|
|
88
|
+
props.store.actions.setSnapshot(hiddenSnapshot)
|
|
89
|
+
render(<PetDockEntry {...props} t={t} />)
|
|
90
|
+
const summon = screen.getByTestId('pet-summon')
|
|
91
|
+
expect(summon.getAttribute('data-dsh-part')).toBe('summon-button')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -277,9 +277,7 @@ describe('PetSprite hover panel placement', () => {
|
|
|
277
277
|
it('lifts the above-panel clear of the session bubble stack', () => {
|
|
278
278
|
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 900 })
|
|
279
279
|
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
|
|
280
|
-
|
|
281
|
-
// session bubble text content being present among its descendants.
|
|
282
|
-
if (this.textContent === '正在思考正在使用 grep') {
|
|
280
|
+
if (this.className.includes('bubbleStack')) {
|
|
283
281
|
// Two stacked bubbles: 88px tall.
|
|
284
282
|
return { top: 600, right: 1200, bottom: 688, left: 1100, width: 100, height: 88, x: 1100, y: 600, toJSON: () => ({}) }
|
|
285
283
|
}
|
|
@@ -327,7 +325,54 @@ describe('PetSprite status bubble', () => {
|
|
|
327
325
|
expect(screen.queryByText('正在思考')).toBeNull()
|
|
328
326
|
})
|
|
329
327
|
|
|
330
|
-
|
|
328
|
+
/** Expand a collapsed session stack by hovering its bubble area. */
|
|
329
|
+
function expandStack(): void {
|
|
330
|
+
const stack = screen.getByText('正在思考').closest('div')
|
|
331
|
+
expect(stack).not.toBeNull()
|
|
332
|
+
fireEvent.pointerOver(stack!)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
it('collapses concurrent sessions behind the display session bubble', () => {
|
|
336
|
+
renderPet({
|
|
337
|
+
snapshot: {
|
|
338
|
+
...workingSnapshot,
|
|
339
|
+
bubble: '正在思考',
|
|
340
|
+
sessions: [
|
|
341
|
+
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考' },
|
|
342
|
+
{ sessionId: 's-b', animation: 'running-right', phase: 'tool', bubble: '正在使用 grep' },
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
// Only the display session speaks; the other session hides behind the
|
|
347
|
+
// '+1' badge so the pet never wears a tall bubble stack.
|
|
348
|
+
expect(screen.getAllByText('正在思考')).toHaveLength(1)
|
|
349
|
+
expect(screen.queryByText('正在使用 grep')).toBeNull()
|
|
350
|
+
expect(screen.queryByRole('button', { name: '展开其余 1 个会话的气泡' })).not.toBeNull()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('expands the collapsed stack on hover and on badge tap', () => {
|
|
354
|
+
renderPet({
|
|
355
|
+
snapshot: {
|
|
356
|
+
...workingSnapshot,
|
|
357
|
+
sessions: [
|
|
358
|
+
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考' },
|
|
359
|
+
{ sessionId: 's-b', animation: 'running-right', phase: 'tool', bubble: '正在使用 grep' },
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
// Hover peek: extras appear, and leaving collapses them again.
|
|
364
|
+
expandStack()
|
|
365
|
+
expect(screen.queryByText('正在使用 grep')).not.toBeNull()
|
|
366
|
+
fireEvent.pointerOut(screen.getByText('正在思考').closest('div')!)
|
|
367
|
+
expect(screen.queryByText('正在使用 grep')).toBeNull()
|
|
368
|
+
// Badge tap pins the stack open (touch path); tapping again collapses.
|
|
369
|
+
fireEvent.click(screen.getByRole('button', { name: '展开其余 1 个会话的气泡' }))
|
|
370
|
+
expect(screen.queryByText('正在使用 grep')).not.toBeNull()
|
|
371
|
+
fireEvent.click(screen.getByRole('button', { name: '收起会话气泡' }))
|
|
372
|
+
expect(screen.queryByText('正在使用 grep')).toBeNull()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('renders one bubble per concurrent session in the expanded stack', () => {
|
|
331
376
|
renderPet({
|
|
332
377
|
snapshot: {
|
|
333
378
|
...workingSnapshot,
|
|
@@ -338,31 +383,43 @@ describe('PetSprite status bubble', () => {
|
|
|
338
383
|
],
|
|
339
384
|
},
|
|
340
385
|
})
|
|
386
|
+
expandStack()
|
|
341
387
|
// The display session appears in the stack exactly once: the legacy
|
|
342
388
|
// single bubble is not rendered on top of the session list.
|
|
343
389
|
expect(screen.getAllByText('正在思考')).toHaveLength(1)
|
|
344
390
|
expect(screen.queryByText('正在使用 grep')).not.toBeNull()
|
|
345
391
|
})
|
|
346
392
|
|
|
347
|
-
it('
|
|
393
|
+
it('lets the inner whisper take over the status bubble', () => {
|
|
348
394
|
renderPet({ snapshot: { ...workingSnapshot, whisper: '哼哧哼哧,大脑转得飞快~' } })
|
|
349
|
-
// The whisper
|
|
395
|
+
// The whisper speaks THROUGH the bubble: it replaces (not accompanies)
|
|
396
|
+
// the status copy, so the pet never shows two bubbles at once.
|
|
350
397
|
expect(screen.queryByText('哼哧哼哧,大脑转得飞快~')).not.toBeNull()
|
|
351
|
-
expect(screen.queryByText('正在思考')).
|
|
398
|
+
expect(screen.queryByText('正在思考')).toBeNull()
|
|
352
399
|
})
|
|
353
400
|
|
|
354
|
-
it('
|
|
355
|
-
renderPet({
|
|
401
|
+
it('lets the whisper take over the display session bubble in the stack', () => {
|
|
402
|
+
const { onOpenSession } = renderPet({
|
|
356
403
|
snapshot: {
|
|
357
404
|
...workingSnapshot,
|
|
358
405
|
whisper: '我在这儿陪着你呢,别急别急',
|
|
359
406
|
sessions: [
|
|
360
407
|
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考' },
|
|
408
|
+
{ sessionId: 's-b', animation: 'running-right', phase: 'tool', bubble: '正在使用 grep' },
|
|
361
409
|
],
|
|
362
410
|
},
|
|
363
411
|
})
|
|
412
|
+
// The primary bubble (the display session) speaks the whisper instead
|
|
413
|
+
// of its status copy; the collapsed extra session hides behind the badge.
|
|
364
414
|
expect(screen.queryByText('我在这儿陪着你呢,别急别急')).not.toBeNull()
|
|
365
|
-
expect(screen.queryByText('正在思考')).
|
|
415
|
+
expect(screen.queryByText('正在思考')).toBeNull()
|
|
416
|
+
expect(screen.queryByText('正在使用 grep')).toBeNull()
|
|
417
|
+
// Expanding reveals the other session's own bubble, untouched.
|
|
418
|
+
fireEvent.pointerOver(screen.getByText('我在这儿陪着你呢,别急别急').closest('div')!)
|
|
419
|
+
expect(screen.queryByText('正在使用 grep')).not.toBeNull()
|
|
420
|
+
// The whisper-toned bubble stays clickable and still opens its session.
|
|
421
|
+
fireEvent.click(screen.getByText('我在这儿陪着你呢,别急别急'))
|
|
422
|
+
expect(onOpenSession).toHaveBeenCalledWith('s-a')
|
|
366
423
|
})
|
|
367
424
|
|
|
368
425
|
it('lets transient interaction feedback take over the whisper too', () => {
|
|
@@ -401,6 +458,8 @@ describe('PetSprite status bubble', () => {
|
|
|
401
458
|
],
|
|
402
459
|
},
|
|
403
460
|
})
|
|
461
|
+
// The collapsed stack must be expanded before its extras can be hit.
|
|
462
|
+
fireEvent.pointerOver(screen.getByText('正在思考').closest('div')!)
|
|
404
463
|
fireEvent.click(screen.getByText('正在使用 grep'))
|
|
405
464
|
expect(onOpenSession).toHaveBeenCalledTimes(1)
|
|
406
465
|
expect(onOpenSession).toHaveBeenCalledWith('s-b')
|
|
@@ -432,7 +491,9 @@ describe('PetSprite status bubble', () => {
|
|
|
432
491
|
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
433
492
|
// The hover panel is open...
|
|
434
493
|
expect(screen.queryByText('改名')).not.toBeNull()
|
|
435
|
-
// ...and the bubbles are still there, still clickable
|
|
494
|
+
// ...and the bubbles are still there, still clickable once the collapsed
|
|
495
|
+
// stack is expanded by hovering it.
|
|
496
|
+
fireEvent.pointerOver(screen.getByText('正在思考').closest('div')!)
|
|
436
497
|
expect(screen.getByText('正在使用 grep')).not.toBeNull()
|
|
437
498
|
fireEvent.click(screen.getByText('正在使用 grep'))
|
|
438
499
|
expect(onOpenSession).toHaveBeenCalledWith('s-b')
|