@clowder-ai/companion 0.1.0-alpha.31 → 0.1.0-alpha.33

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.en.md ADDED
@@ -0,0 +1,51 @@
1
+ # Companion
2
+
3
+ [Chinese](README.md) · English
4
+
5
+ `@clowder-ai/companion` is the desktop voice body for a Clowder AI companion: a small
6
+ always-on-top desktop window (`renderer/index.html`) through which you talk, think, and
7
+ find your shared history together. It ships as a Manager-installable builtin plugin
8
+ (`runtime.transport: builtin`) and contributes a single `desktop-window` surface named
9
+ `companion`.
10
+
11
+ ## Authority boundary
12
+
13
+ The Host owns installation, grants, configuration and secrets, window/admission policy,
14
+ and lifecycle supervision. The package declares a window surface and ships only
15
+ renderer assets; it never mints handles and never reads ambient Host state.
16
+
17
+ ## Surface interaction
18
+
19
+ At rest the desktop shows only your selected cat. Click the cat for voice or text;
20
+ right-click for history, screen sharing, household access, sound and hiding.
21
+ Dragging moves the cat and folds its controls. Escape folds a panel without
22
+ ending the conversation or clearing its draft.
23
+
24
+ Voice requires an explicit **Voice chat** click. An active microphone badge remains
25
+ visible and ends the call in one click. Screen sharing has its own explicit
26
+ picker and termination badge. There is no microphone action on a double-click,
27
+ page load, body tap or drag.
28
+
29
+ When a compatible Host advertises receive-only audio, **Listen only** opens the same
30
+ conversation with playback but never requests or toggles a microphone. Older
31
+ Hosts omit that capability, so the control stays hidden and ordinary voice keeps
32
+ the exact legacy connect request. Text remains available independently.
33
+
34
+ Typing works with voice off. The Host uses the existing owner conversation,
35
+ configured duty cat, ordinary message routing and idempotency. The small history
36
+ view reads a bounded recent subset; **Full conversation** opens the canonical conversation.
37
+ No second transcript database, credentials or selectable Host identity live here.
38
+ When the Host supplies a valid per-message identity snapshot, each history row
39
+ keeps its real author while separately showing the partner, Live carrier and
40
+ deep cat saved with that message. Legacy rows omit this context instead of
41
+ borrowing today's selected companion.
42
+
43
+ The Host owns installation, grants, credentials and lifecycle; saving configuration preserves enable intent. The language link opens a separate document and does not imply automatic language selection.
44
+
45
+ ## Compatibility and durable input
46
+
47
+ The desktop surface requires Host bridge 1.6.0; an older Host refuses installation. The Host controls placement, dragging, transparent hit areas, media admission and capture. The plugin receives no raw streams or provider credentials. Hiding releases media; the ordinary Chat entry restores the cat.
48
+
49
+ Drafts are Host-owned and durable. An unknown send result is read back, never automatically resent. An explicit retry retains the original identity, content and route. A changed canonical conversation invalidates old unconfirmed text rather than replaying it into the new scope. History loading, response phase and body completeness remain separate.
50
+
51
+ Application assistance is off by default. Settling a prompt opens the ordinary menu without granting screen or call authority. Cat poses use current Host facts; dragging suspends autonomous movement. Reduced motion keeps poses static. Automatic play is bounded by idle and cooldown timers; pounce requires an explicit gesture by default.
package/README.md CHANGED
@@ -1,86 +1,27 @@
1
- # Companion surface
1
+ # 猫猫球
2
2
 
3
- At rest the desktop shows only your selected cat. Click the cat for voice or text;
4
- right-click for history, screen sharing, household access, sound and hiding.
5
- Dragging moves the cat and folds its controls. Escape folds a panel without
6
- ending the conversation or clearing its draft.
3
+ 简体中文 · [English](README.en.md)
7
4
 
8
- Voice requires an explicit **语音聊** click. An active microphone badge remains
9
- visible and ends the call in one click. Screen sharing has its own explicit
10
- picker and termination badge. There is no microphone action on a double-click,
11
- page load, body tap or drag.
5
+ 在桌面上与你的猫语音或文字交流,查看共同历史,并按需共享屏幕。猫猫球是置顶、透明、无边框的小窗口;需要兼容的桌面宿主。
12
6
 
13
- When a compatible Host advertises receive-only audio, **只听** opens the same
14
- conversation with playback but never requests or toggles a microphone. Older
15
- Hosts omit that capability, so the control stays hidden and ordinary voice keeps
16
- the exact legacy connect request. Text remains available independently.
7
+ ## 开始使用
17
8
 
18
- Typing works with voice off. The Host uses the existing owner conversation,
19
- configured duty cat, ordinary message routing and idempotency. The small history
20
- view reads a bounded recent subset; **完整聊天** opens the canonical conversation.
21
- No second transcript database, credentials or selectable Host identity live here.
22
- When the Host supplies a valid per-message identity snapshot, each history row
23
- keeps its real author while separately showing the partner, Live carrier and
24
- deep cat saved with that message. Legacy rows omit this context instead of
25
- borrowing today's selected companion.
9
+ 1. 安装并启用 `@clowder-ai/companion`,通过 Clowder AI 的“聊聊”入口打开猫猫球。
10
+ 2. 点击猫打开语音或文字控制;右键查看历史、屏幕共享、家园、声音和隐藏选项。拖动猫移动窗口,Escape 收起面板而不结束对话或清空草稿。
11
+ 3. 只有明确点击“语音聊”才开启麦克风;通话时可通过始终可见的标记一键结束。屏幕共享另有明确的选择与结束操作。
26
12
 
27
- ## Host integration
13
+ 兼容宿主提供“只听”时,可以接收同一对话的音频而不申请麦克风。较旧宿主不显示这个入口。语音关闭时仍可独立输入文字。
28
14
 
29
- This surface requires the additive desktop bridge **1.6.0**. The Host computes
30
- placement within the display work area, keeps the cat as its stable anchor, and
31
- owns bounded drag gestures and transparent-area hit testing. Hosts below 1.6
32
- reject this manifest before installation. Media admission and capture
33
- remain in the trusted Host; this package receives neither streams nor provider
34
- SDP. Hiding releases media; the existing Clowder **聊聊** entry restores the cat.
35
- Bridge 1.4 adds one untrusted first-visible-subtitle observation containing only
36
- an epoch-millisecond time, the current call fence and a Host-correlatable source
37
- reference. It does not claim compositor paint, an unoccluded window or reading.
38
- Message and native-item references use the same bounded, single-line opaque ID
39
- contract as the transcript surfaces that supply them. A Host
40
- `invalid_request` rejects and remembers that candidate for the current call so
41
- another current reference may be observed without retrying the rejected one.
42
- Other failures retry after 1 and 2 seconds, then leave the call's
43
- first-visible result unknown; later text never backfills a manufactured success.
44
- Bridge 1.5 adds Host-bound conversation pages and versioned body continuations.
45
- The renderer can only return opaque cursors issued by the Host; it cannot select
46
- an owner, thread, message, call or byte offset. History progress, response phase
47
- and body completeness remain separate, so a processing draft or truncated body
48
- cannot masquerade as a completed reply.
15
+ ## 历史与草稿
49
16
 
50
- It also binds the one-line composer to the Host-owned durable draft. Versioned
51
- text submission carries the Host-issued conversation `scopeRef`, draft revision
52
- and caller message identity. An unknown settlement is read back but never
53
- automatically resent; an explicit retry keeps the original text, identity,
54
- revision and route. If the canonical thread changes, the old unconfirmed input
55
- stays visible as invalidated local text and is never replayed into the new scope.
56
- The legacy text and conversation methods remain unchanged for older pairs.
17
+ 小窗显示有限数量的近期历史,“完整聊天”打开对应的正式对话。每条消息沿用宿主提供的真实作者与当时角色信息,不用当前选择的猫替换历史身份。
57
18
 
58
- Bridge 1.6 separates passive call speech from the bounded nameplate and control
59
- panel, and adds an optional application-assistance projection. Assistance is
60
- off by default; settling a prompt only opens the ordinary menu and grants no
61
- media, screen-selection or call authority.
19
+ 草稿由宿主持久保存。发送结果不明时先读回状态,不自动重发;明确重试沿用原消息身份和内容。切换正式对话后,旧的未确认输入保留为已失效文字,不会自动发进新对话。历史加载进度、回复处理状态与正文是否完整分别展示。
62
20
 
63
- Xianxian uses one layered sitting body or one VP9 clip at a time. Expiring Host
64
- facts with task, result and event identities select tool work, workspace fetch,
65
- screen reading and applied-result delivery. Native travel is independent: the
66
- renderer observes the actual desktop window coordinates, so a running clip can
67
- appear only after the Host window really moves. Dragging suppresses that
68
- observer and freezes a visibly held candidate instead of renaming the running
69
- clip. Pounce requires an explicit play gesture by default; optional automatic
70
- play is bounded by idle and cooldown timers. Reduced motion keeps semantic
71
- poses static; a compatible Host also revokes its native movement lease.
72
- Other cats retain their own skins. The v3 living assets and their hashes are
73
- pinned in `source-lock.json`; the installable renderer carries all 21 assets
74
- itself, including the mid-edge `peek_fade` variant.
21
+ ## 桌面行为与隐私
75
22
 
76
- The Host keeps a small transparent margin left of the pet hit target so the
77
- pounce's tail is visible without turning empty space into a click target.
23
+ 需要桌面桥接版本 1.6.0;不兼容的宿主会在安装前拒绝。宿主控制窗口位置、拖动与透明区域点击,负责媒体授权和采集;插件不接收原始媒体流或提供方会话凭据。隐藏窗口会释放媒体资源,重新通过“聊聊”打开。
78
24
 
79
- ## Verification
25
+ 应用协助默认关闭,确认提示只打开普通菜单,不会自动授权屏幕或通话。角色动画受宿主当前事实驱动;拖动时暂停自主移动,减少动态效果设置保留静态姿态。自动玩耍受空闲和冷却时间约束,默认扑跃需要明确操作。
80
26
 
81
- Run the repository's contract and SDK builds, then `pnpm --filter
82
- @clowder-ai/companion test` and `pnpm --filter @clowder-ai/companion lint`.
83
- `node packages/companion/scripts/preview.mjs` serves the accepted interaction
84
- spike at `/spike/` and the actual built renderer with an explicitly simulated
85
- bridge at `/previous`. The latter covers visual states and text controls; it
86
- does not prove native movement, real capture or model delivery.
27
+ 宿主管理安装、授权和启停。英文文档是独立文件,互链不代表页面已支持自动语言切换。
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.33",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -40,7 +40,7 @@
40
40
  "bridgeVersion": "1.6.0",
41
41
  "surface": {
42
42
  "entrypoint": "renderer/index.html",
43
- "integrity": "sha256-dR1cqLYEPsY28TJ6ureljiV5CP23wubn70G7pePPkNg="
43
+ "integrity": "sha256-0OpVp+p2y+xDJduVuzWZf48TNLBnWZZfq/TuGe0joyY="
44
44
  },
45
45
  "presentation": {
46
46
  "width": 300,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clowder-ai/companion",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.33",
4
4
  "description": "The desktop voice body for your Clowder companion",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  "manifest.json",
11
11
  "LICENSE",
12
12
  "NOTICE",
13
- "source-lock.json"
13
+ "source-lock.json",
14
+ "README.en.md"
14
15
  ],
15
16
  "exports": {
16
17
  "./manifest": "./manifest.json"
@@ -27,8 +28,8 @@
27
28
  "prepack": "pnpm build"
28
29
  },
29
30
  "devDependencies": {
30
- "@clowder-ai/plugin-contract": "0.1.0-beta.29",
31
- "@clowder-ai/plugin-sdk": "0.1.0-beta.25",
31
+ "@clowder-ai/plugin-contract": "0.1.0-beta.37",
32
+ "@clowder-ai/plugin-sdk": "0.2.0-beta.21",
32
33
  "playwright": "1.61.1"
33
34
  },
34
35
  "repository": {
package/plugin.yaml CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.33",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -40,7 +40,7 @@
40
40
  "bridgeVersion": "1.6.0",
41
41
  "surface": {
42
42
  "entrypoint": "renderer/index.html",
43
- "integrity": "sha256-dR1cqLYEPsY28TJ6ureljiV5CP23wubn70G7pePPkNg="
43
+ "integrity": "sha256-0OpVp+p2y+xDJduVuzWZf48TNLBnWZZfq/TuGe0joyY="
44
44
  },
45
45
  "presentation": {
46
46
  "width": 300,
@@ -1,4 +1,5 @@
1
1
  const ACTIONS = new Set(['dismiss', 'timeout', 'accept']);
2
+ export const APP_ASSIST_TIMEOUT_MS = 15_000;
2
3
 
3
4
  /**
4
5
  * Renderer projection only. The Host owns offer creation, expiry and clearing;
@@ -12,7 +13,7 @@ export class AppAssistController {
12
13
  accepted = () => {},
13
14
  scheduleTimeout = setTimeout,
14
15
  cancelTimeout = clearTimeout,
15
- timeoutMs = 15_000,
16
+ timeoutMs = APP_ASSIST_TIMEOUT_MS,
16
17
  }) {
17
18
  this.client = client;
18
19
  this.changed = changed;
@@ -0,0 +1,16 @@
1
+ import { APP_ASSIST_TIMEOUT_MS } from './app-assist-controller.mjs';
2
+
3
+ /** Projects the existing renderer deadline without owning or extending it. */
4
+ export function bindAppAssistCountdown(element, durationMs = APP_ASSIST_TIMEOUT_MS) {
5
+ let offerRef = null;
6
+ element.style.setProperty('--app-assist-duration', `${durationMs}ms`);
7
+ return offer => {
8
+ const nextRef = offer?.offerRef ?? null;
9
+ if (nextRef === offerRef) return;
10
+ offerRef = nextRef;
11
+ element.removeAttribute('data-active');
12
+ if (!nextRef) return;
13
+ void element.getBoundingClientRect?.();
14
+ element.setAttribute('data-active', 'true');
15
+ };
16
+ }
@@ -9,5 +9,8 @@ const messages = {
9
9
  };
10
10
  export function explainError(error) {
11
11
  if (error?.name === 'NotAllowedError') return '未获得麦克风权限 · 请在系统设置中允许后重试';
12
- return messages[error?.code] ?? '连接暂时不可用 · 请重试或打开聊天继续';
12
+ // hasOwn: `??` does not stop a truthy prototype hit — {code:'constructor'}
13
+ // would render `function Object() { [native code] }` as the user message.
14
+ if (Object.hasOwn(messages, error?.code)) return messages[error.code];
15
+ return '连接暂时不可用 · 请重试或打开聊天继续';
13
16
  }
@@ -48,6 +48,10 @@
48
48
  <p id="bubble-first" hidden></p><p id="bubble-second" hidden></p>
49
49
  </section>
50
50
  <section id="app-assist" class="floating app-assist" aria-label="应用旁的轻提示" data-candidate="true" hidden>
51
+ <svg id="app-assist-countdown" class="app-assist-countdown" viewBox="0 0 20 20" role="img" aria-label="提示将在约十五秒后自动收起">
52
+ <circle class="app-assist-countdown-track" cx="10" cy="10" r="7.5" pathLength="1"/>
53
+ <circle id="app-assist-countdown-progress" cx="10" cy="10" r="7.5" pathLength="1"/>
54
+ </svg>
51
55
  <p>你打开了 <strong id="app-assist-name"></strong>。要我一起看吗?</p>
52
56
  <div><button id="app-assist-dismiss" type="button">暂不</button><button id="app-assist-start" class="primary" type="button">打开通话控制</button></div>
53
57
  </section>
@@ -79,10 +79,19 @@ button:disabled { opacity: .45; cursor: default; }
79
79
  #bubble p + p { margin-top: 4px; }
80
80
  #bubble [data-role="user"] { color: var(--cafe-text-secondary); }
81
81
  .app-assist { width: 320px; padding: 12px; }
82
- .app-assist p { margin: 0; color: var(--desktop-body); font-size: 13px; line-height: 1.5; }
82
+ .app-assist p { margin: 0; padding-right: 22px; color: var(--desktop-body); font-size: 13px; line-height: 1.5; }
83
83
  .app-assist p strong { color: var(--desktop-ink); font-weight: 600; }
84
84
  .app-assist > div { display: flex; justify-content: flex-end; gap: 6px; margin-top: 9px; }
85
85
  .app-assist > div button { min-height: 30px; padding: 5px 10px; font-size: 12px; }
86
+ .app-assist-countdown { position: absolute; top: 10px; right: 10px; width: 16px; height: 16px; overflow: visible; transform: rotate(-90deg); }
87
+ .app-assist-countdown circle { fill: none; stroke-width: 2; }
88
+ .app-assist-countdown-track { stroke: var(--desktop-line-strong); }
89
+ #app-assist-countdown-progress { stroke: var(--desktop-muted); stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 0; }
90
+ .app-assist-countdown[data-active="true"] #app-assist-countdown-progress { animation: app-assist-expiry var(--app-assist-duration) linear forwards; }
91
+ @keyframes app-assist-expiry { to { stroke-dashoffset: 1; } }
92
+ @media (prefers-reduced-motion: reduce) {
93
+ .app-assist-countdown[data-active="true"] #app-assist-countdown-progress { animation-timing-function: steps(15, end); }
94
+ }
86
95
  #call-bubble { width: 320px; max-height: 80px; overflow: hidden; padding: 9px 11px; pointer-events: none; }
87
96
  #call-bubble:not(:has(p:not([hidden]))) { display: none; }
88
97
  #call-bubble p { margin: 0; display: flex; min-width: 0; overflow: hidden; white-space: nowrap; font-size: 12px; line-height: 1.45; }
@@ -24,6 +24,7 @@ import { ConversationSurface } from './conversation-surface.mjs';
24
24
  import { CallControlsPresentation } from './call-controls-presentation.mjs';
25
25
  import { CallDuration } from './call-duration.mjs';
26
26
  import { AppAssistController } from './app-assist-controller.mjs';
27
+ import { bindAppAssistCountdown } from './app-assist-countdown.mjs';
27
28
  const $ = id => document.getElementById(id);
28
29
  const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
29
30
  const status = text => {
@@ -61,6 +62,7 @@ let previousControlPanel = 'none';
61
62
  const ambientPanel = active => active ? 'actions' : appAssistOffer ? 'app-assist' : 'none';
62
63
  const setTexts = (ids, text) => { for (const id of ids) $(id).textContent = text; };
63
64
  const setHidden = (ids, hidden) => { for (const id of ids) $(id).hidden = hidden; };
65
+ const renderAppAssistCountdown = bindAppAssistCountdown($('app-assist-countdown'));
64
66
  if (!window.clowderCompanion) {
65
67
  $('actions').hidden = false; $('status').hidden = false;
66
68
  status('请从 Clowder 的聊聊入口打开猫猫球');
@@ -144,6 +146,7 @@ if (!window.clowderCompanion) {
144
146
  changed: value => {
145
147
  appAssistOffer = value.offer;
146
148
  $('app-assist-name').textContent = appAssistOffer?.appName ?? '';
149
+ renderAppAssistCountdown(appAssistOffer);
147
150
  void controls.setAmbient(ambientPanel(previousPhase !== 'idle'));
148
151
  },
149
152
  accepted: () => {