@steerable/agent-shell-web 0.6.70 → 0.6.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.70",
3
+ "version": "0.6.72",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
@@ -45,8 +45,8 @@
45
45
  "react-router-dom": "^6.30.0",
46
46
  "rehype-raw": "^7.0.0",
47
47
  "remark-gfm": "^4.0.1",
48
- "@steerable/agent-protocol": "0.6.70",
49
- "@steerable/agent-ui": "0.6.70"
48
+ "@steerable/agent-protocol": "0.6.72",
49
+ "@steerable/agent-ui": "0.6.72"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.0",
@@ -59,7 +59,7 @@
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^5.4.21",
61
61
  "vitest": "^3.2.4",
62
- "@steerable/pack-sdk": "0.6.70"
62
+ "@steerable/pack-sdk": "0.6.72"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
Binary file
Binary file
package/src/brand.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * (VITE_BRAND_* define:product.json brand 优先,其次激活包 pack.json
4
4
  * brand,3.1);logo 资产由激活包的 web 模块在注册时经
5
5
  * setBrandLogoUrl() 注入(资产静态 import 在包内,只进本产品 bundle)。
6
- * 未注入时一律回落到 shell 默认(Steerable Shell / 舵轮标)。
6
+ * 未注入时一律回落到 shell 默认(Steerable Shell:方形标 + 横版字标)。
7
7
  * 主进程侧对应物见 src/brand.ts。
8
8
  */
9
- import shellLogoUrl from '@/assets/logo-steerable.svg';
9
+ import shellMarkUrl from '@/assets/logo-steerable-mark.png';
10
+ import shellWordmarkUrl from '@/assets/logo-steerable-wordmark.png';
10
11
 
11
12
  /** 当前构建的 flavor(开放字符串,0.3g 起不再是二值联合)。 */
12
13
  export const APP_FLAVOR: string = import.meta.env.VITE_APP_FLAVOR ?? 'generic';
@@ -30,25 +31,35 @@ export const BRAND_TITLE: string =
30
31
  export const BRAND_HOME_HINT: string =
31
32
  import.meta.env.VITE_BRAND_HOME_HINT ?? 'Type a message to start a new chat.';
32
33
 
33
- /** shell 默认 logo(Steerable 舵轮标);包品牌 logo 由包 web 模块注册覆盖。 */
34
- let brandLogoUrl: string = shellLogoUrl;
34
+ /** shell 默认方形标,用于侧栏和 favicon。包只注册一枚 logo 时与字标相同。 */
35
+ let brandMarkUrl: string = shellMarkUrl;
36
+
37
+ /** shell 默认横版字标,用于空对话首屏。包品牌 logo 由包 web 模块注册覆盖。 */
38
+ let brandLogoUrl: string = shellWordmarkUrl;
35
39
 
36
40
  /**
37
41
  * 注册产品品牌 logo(包的 register*Renderer() 在 bootstrap 前调用)。
38
42
  * 重复注册抛错(组装期笔误,fail fast——与 node 侧 setProductBrand 同语义)。
43
+ * 包只提供一枚图,方形标和字标都用它。
39
44
  */
40
45
  export function setBrandLogoUrl(url: string): void {
41
- if (brandLogoUrl !== shellLogoUrl && brandLogoUrl !== url) {
46
+ if (brandLogoUrl !== shellWordmarkUrl && brandLogoUrl !== url) {
42
47
  throw new Error('[brand] logo already set');
43
48
  }
44
49
  brandLogoUrl = url;
50
+ brandMarkUrl = url;
45
51
  }
46
52
 
47
- /** 当前产品品牌 logo(bootstrap 之后读取——包的注册已完成)。 */
53
+ /** 当前产品品牌字标(bootstrap 之后读取——包的注册已完成)。 */
48
54
  export function getBrandLogoUrl(): string {
49
55
  return brandLogoUrl;
50
56
  }
51
57
 
58
+ /** 当前产品方形标。shell 默认是彩色 S;包覆盖后与字标相同。 */
59
+ export function getBrandMarkUrl(): string {
60
+ return brandMarkUrl;
61
+ }
62
+
52
63
  /** 首页 / 新对话未选手动专家时绑定的内置智能体(包品牌可覆盖)。 */
53
64
  export const DEFAULT_AGENT_ID: string =
54
65
  import.meta.env.VITE_DEFAULT_AGENT_ID ?? LOCAL_ASSISTANT_AGENT_ID;
@@ -1,4 +1,4 @@
1
- import { BRAND_NAME, BRAND_TITLE, getBrandLogoUrl } from '@/brand';
1
+ import { BRAND_NAME, BRAND_TITLE, getBrandMarkUrl } from '@/brand';
2
2
 
3
3
  export interface BrandLockupProps {
4
4
  className?: string;
@@ -24,7 +24,7 @@ export function BrandLockup({ className, onClick }: BrandLockupProps = {}) {
24
24
  .join(' ')}
25
25
  >
26
26
  <img
27
- src={getBrandLogoUrl()}
27
+ src={getBrandMarkUrl()}
28
28
  alt={title || BRAND_NAME}
29
29
  className={`${title ? 'h-5' : 'h-6'} w-auto max-w-[10rem] flex-shrink-0 select-none object-contain object-left`}
30
30
  draggable={false}
@@ -2,7 +2,7 @@ import { BRAND_HOME_HINT, BRAND_NAME, getBrandLogoUrl } from '@/brand';
2
2
  import { t } from '@/i18n';
3
3
 
4
4
  /**
5
- * 新对话落地页与空会话首屏的品牌区:侧栏同一枚 logo,加一句
5
+ * 新对话落地页与空会话首屏的品牌区:横版字标,加一句
6
6
  * 产品可配的副文案(brand.homeHint)。
7
7
  */
8
8
  export function EmptyChatHero() {
@@ -13,7 +13,7 @@ export function EmptyChatHero() {
13
13
  <img
14
14
  src={getBrandLogoUrl()}
15
15
  alt={BRAND_NAME}
16
- className="mx-auto h-12 w-auto max-w-[12rem] select-none object-contain"
16
+ className="mx-auto h-16 w-auto max-w-[18rem] select-none object-contain"
17
17
  draggable={false}
18
18
  />
19
19
  </h1>
@@ -83,17 +83,21 @@ function TaskOutcomeCard({
83
83
 
84
84
  <div className="min-w-0 flex-1">
85
85
  <div
86
- className={
86
+ data-task-outcome-title=""
87
+ className={`line-clamp-2 break-words ${
87
88
  failed
88
89
  ? 'text-red-700 dark:text-red-300'
89
90
  : 'text-emerald-700 dark:text-emerald-300'
90
- }
91
+ }`}
91
92
  >
92
93
  {failed ? t('Background task failed: ') : t('Background task completed: ')}
93
94
  <span className="font-medium">{task.task}</span>
94
95
  </div>
95
96
  {detail && (
96
- <div className="mt-1 line-clamp-3 whitespace-pre-wrap text-agent-muted-foreground">
97
+ <div
98
+ data-task-outcome-detail=""
99
+ className="mt-1 line-clamp-2 break-words text-agent-muted-foreground"
100
+ >
97
101
  {detail}
98
102
  </div>
99
103
  )}
@@ -129,6 +129,18 @@ export interface PythonRunnerSnapshot {
129
129
  restartRequired: boolean;
130
130
  }
131
131
 
132
+ export interface HostClipboardFile {
133
+ name: string;
134
+ path?: string;
135
+ dataBase64?: string;
136
+ mime?: string;
137
+ }
138
+
139
+ export interface HostClipboard {
140
+ text: string;
141
+ files: HostClipboardFile[];
142
+ }
143
+
132
144
  export interface HostBridge {
133
145
  runtime: 'local';
134
146
  platform: NodeJS.Platform;
@@ -197,25 +209,17 @@ export interface HostBridge {
197
209
  offMenuOpenTerminal?: () => void;
198
210
  /**
199
211
  * App menu IPC — `Cmd+V` / 编辑 → 粘贴 fires `menu:paste`.
200
- * WKWebView does not expose that pasteboard to the page, so the renderer
201
- * reads it through `readClipboard`.
212
+ * WKWebView does not expose that pasteboard to the page, so the host reads
213
+ * it before emitting the event.
202
214
  */
203
- onMenuPaste?: (callback: () => void) => void;
215
+ onMenuPaste?: (callback: (clipboard: HostClipboard) => void) => void;
204
216
  offMenuPaste?: () => void;
205
217
  /**
206
218
  * OS pasteboard. `files` are copied paths or a screenshot PNG.
207
219
  * Text is empty when the pasteboard holds files, so the path is not
208
220
  * inserted into the composer as well.
209
221
  */
210
- readClipboard?: () => Promise<{
211
- text: string;
212
- files: Array<{
213
- name: string;
214
- path?: string;
215
- dataBase64?: string;
216
- mime?: string;
217
- }>;
218
- }>;
222
+ readClipboard?: () => Promise<HostClipboard>;
219
223
  /** Plain text from the OS pasteboard. Empty when the clipboard has no text. */
220
224
  readClipboardText?: () => Promise<string>;
221
225
  /**
@@ -4,18 +4,13 @@
4
4
  * and writes it into the focused field.
5
5
  */
6
6
 
7
- export interface HostClipboardFile {
8
- name: string;
9
- path?: string;
10
- dataBase64?: string;
11
- mime?: string;
12
- }
7
+ import {
8
+ getHostBridge,
9
+ type HostClipboard,
10
+ type HostClipboardFile,
11
+ } from './host-bridge';
13
12
 
14
- export interface HostClipboard {
15
- text: string;
16
- files: HostClipboardFile[];
17
- }
18
- import { getHostBridge } from './host-bridge';
13
+ export type { HostClipboardFile } from './host-bridge';
19
14
 
20
15
  let pending = false;
21
16
  let lastEditable: HTMLElement | null = null;
@@ -44,8 +39,8 @@ export function installHostPaste(): void {
44
39
  if (isEditable(target)) lastEditable = target;
45
40
  });
46
41
  const bridge = getHostBridge();
47
- bridge?.onMenuPaste?.(() => {
48
- requestHostPaste();
42
+ bridge?.onMenuPaste?.((clipboard) => {
43
+ insertHostClipboard(clipboard);
49
44
  });
50
45
  }
51
46
 
@@ -58,8 +53,7 @@ export function requestHostPaste(target?: HTMLElement | null): void {
58
53
  pending = true;
59
54
  void readHostClipboard(readClipboard, readText)
60
55
  .then((clip) => {
61
- if (clip.files.length > 0) insertHostFiles(clip.files, target ?? null);
62
- if (clip.text) insertHostText(clip.text, target ?? null);
56
+ insertHostClipboard(clip, target ?? null);
63
57
  })
64
58
  .catch((error: unknown) => {
65
59
  console.warn('[host-paste] clipboard read failed', error);
@@ -100,6 +94,11 @@ export function hostClipboardAvailable(): boolean {
100
94
  );
101
95
  }
102
96
 
97
+ function insertHostClipboard(clipboard: HostClipboard, target: HTMLElement | null = null): void {
98
+ if (clipboard.files.length > 0) insertHostFiles(clipboard.files, target);
99
+ if (clipboard.text) insertHostText(clipboard.text, target);
100
+ }
101
+
103
102
  function isEditable(target: EventTarget | null): target is HTMLElement {
104
103
  if (isTerminalInput(target) || !(target instanceof HTMLElement)) return false;
105
104
  if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return true;
@@ -10,22 +10,23 @@ import { listen, type UnlistenFn } from '@tauri-apps/api/event';
10
10
  import html2canvas from 'html2canvas';
11
11
  import type {
12
12
  AppReleaseSnapshot,
13
+ HostClipboard,
13
14
  HostBridge,
14
15
  PythonRunnerSnapshot,
15
16
  } from './host-bridge';
16
17
  import { createHttpBridge } from './http-bridge';
17
18
 
18
- type VoidCallback = () => void;
19
+ type EventCallback<T> = (payload: T) => void;
19
20
 
20
- function createMenuSubscription(event: string): {
21
- on(callback: VoidCallback): void;
21
+ function createMenuSubscription<T = void>(event: string): {
22
+ on(callback: EventCallback<T>): void;
22
23
  off(): void;
23
24
  } {
24
- const subscriptions = new Map<VoidCallback, Promise<UnlistenFn>>();
25
+ const subscriptions = new Map<EventCallback<T>, Promise<UnlistenFn>>();
25
26
  return {
26
27
  on(callback) {
27
28
  if (subscriptions.has(callback)) return;
28
- const subscription = listen(event, () => callback());
29
+ const subscription = listen<T>(event, ({ payload }) => callback(payload));
29
30
  subscriptions.set(callback, subscription);
30
31
  },
31
32
  off() {
@@ -42,7 +43,7 @@ export function createTauriBridge(): HostBridge {
42
43
  const bridge = createHttpBridge();
43
44
  const newChatMenu = createMenuSubscription('menu:new-chat');
44
45
  const terminalMenu = createMenuSubscription('menu:open-terminal');
45
- const pasteMenu = createMenuSubscription('menu:paste');
46
+ const pasteMenu = createMenuSubscription<HostClipboard>('menu:paste');
46
47
 
47
48
  return {
48
49
  ...bridge,
@@ -92,11 +93,7 @@ export function createTauriBridge(): HostBridge {
92
93
  offMenuOpenTerminal: terminalMenu.off,
93
94
  onMenuPaste: pasteMenu.on,
94
95
  offMenuPaste: pasteMenu.off,
95
- readClipboard: () =>
96
- invoke<{
97
- text: string;
98
- files: Array<{ name: string; path?: string; dataBase64?: string; mime?: string }>;
99
- }>('host_read_clipboard'),
96
+ readClipboard: () => invoke<HostClipboard>('host_read_clipboard'),
100
97
  readClipboardText: () => invoke<string>('host_read_clipboard_text'),
101
98
  app: {
102
99
  snapshot: () => invoke<AppReleaseSnapshot>('app_release_snapshot'),
package/src/main.tsx CHANGED
@@ -7,7 +7,7 @@ import { AgentLayout } from './layouts/AgentLayout';
7
7
  import { AgentPage } from './pages/AgentPage';
8
8
  import { SettingsPage } from './pages/SettingsPage';
9
9
  import { getPackRoutes } from './packs/registry';
10
- import { getBrandLogoUrl, BRAND_NAME } from './brand';
10
+ import { getBrandMarkUrl, BRAND_NAME } from './brand';
11
11
  import { getAppShellGate } from './auth/gate';
12
12
  import { installHostPaste } from './lib/host-paste';
13
13
  import './styles/index.css';
@@ -107,7 +107,11 @@ export async function bootstrap(): Promise<void> {
107
107
  document.documentElement.lang = getLocale() === 'zh' ? 'zh-CN' : getLocale();
108
108
  window.steerableHost?.setLocale?.(getLocale());
109
109
  document.title = BRAND_NAME;
110
- document.querySelector<HTMLLinkElement>('link[rel="icon"]')?.setAttribute('href', getBrandLogoUrl());
110
+ const icon = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
111
+ if (icon) {
112
+ icon.type = 'image/png';
113
+ icon.href = getBrandMarkUrl();
114
+ }
111
115
  installHostPaste();
112
116
 
113
117
  const rootEl = document.getElementById('root');
@@ -1,14 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
2
- <circle cx="24" cy="24" r="17" stroke="#5c6bc0" stroke-width="3.5"/>
3
- <circle cx="24" cy="24" r="5" fill="#5c6bc0"/>
4
- <g stroke="#5c6bc0" stroke-width="3.5" stroke-linecap="round">
5
- <line x1="24" y1="2.5" x2="24" y2="12"/>
6
- <line x1="24" y1="36" x2="24" y2="45.5"/>
7
- <line x1="2.5" y1="24" x2="12" y2="24"/>
8
- <line x1="36" y1="24" x2="45.5" y2="24"/>
9
- <line x1="8.8" y1="8.8" x2="15.5" y2="15.5"/>
10
- <line x1="32.5" y1="32.5" x2="39.2" y2="39.2"/>
11
- <line x1="39.2" y1="8.8" x2="32.5" y2="15.5"/>
12
- <line x1="15.5" y1="32.5" x2="8.8" y2="39.2"/>
13
- </g>
14
- </svg>
@@ -1,14 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
2
- <circle cx="24" cy="24" r="17" stroke="#5c6bc0" stroke-width="3.5"/>
3
- <circle cx="24" cy="24" r="5" fill="#5c6bc0"/>
4
- <g stroke="#5c6bc0" stroke-width="3.5" stroke-linecap="round">
5
- <line x1="24" y1="2.5" x2="24" y2="12"/>
6
- <line x1="24" y1="36" x2="24" y2="45.5"/>
7
- <line x1="2.5" y1="24" x2="12" y2="24"/>
8
- <line x1="36" y1="24" x2="45.5" y2="24"/>
9
- <line x1="8.8" y1="8.8" x2="15.5" y2="15.5"/>
10
- <line x1="32.5" y1="32.5" x2="39.2" y2="39.2"/>
11
- <line x1="39.2" y1="8.8" x2="32.5" y2="15.5"/>
12
- <line x1="15.5" y1="32.5" x2="8.8" y2="39.2"/>
13
- </g>
14
- </svg>