Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions apps/desktop/src/lib/hosted-search-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ export function hostedSearchHost(url: string): string {
export function hostedSearchFaviconCandidates(url: string): string[] {
try {
const parsed = new URL(url);
const host = parsed.hostname;
const remote = `https://a.favicon.im/${encodeURIComponent(host)}?larger=true`;
return parsed.protocol === "https:"
? [`${parsed.origin}/favicon.ico`, remote]
: [remote];
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return [];
return [`${parsed.origin}/favicon.ico`];
} catch {
return [];
}
Expand Down Expand Up @@ -90,11 +87,7 @@ export function sourcesForHref(
}
return matched;
}
const direct = sources.filter((source) => urlsReferToSameSource(source.url, href));
if (direct.length > 0) return direct;
const host = hostedSearchHost(href);
if (!host) return [];
return sources.filter((source) => hostedSearchHost(source.url) === host);
return sources.filter((source) => urlsReferToSameSource(source.url, href));
}

export function openChatHttpUrl(url: string): void {
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/test/hosted-search-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
register(pathToFileURL(join(here, "helpers/ts-import-hooks.mjs")));
const {
hostedSearchFaviconCandidates,
hostedSearchTitle,
rewriteInlineCitationMarkup,
sourcesForHref,
Expand Down Expand Up @@ -61,3 +62,30 @@ test("resolves #cite indices as 1-based source rows", () => {
const matched = sourcesForHref("#cite=4", sources);
assert.equal(matched[0]?.url, "https://www.bing.com/search?q=tibo");
});

test("resolves markdown links only when host and path match a source", () => {
const matched = sourcesForHref("https://example.com/a", sources);
assert.equal(matched.length, 1);
assert.equal(matched[0]?.url, "https://example.com/a");
});

test("does not turn same-host different-path links into citations", () => {
assert.deepEqual(sourcesForHref("https://example.com", sources), []);
assert.deepEqual(sourcesForHref("https://example.com/other", sources), []);
});

test("loads favicons only from the source origin", () => {
assert.deepEqual(hostedSearchFaviconCandidates("https://news.example.com/c"), [
"https://news.example.com/favicon.ico",
]);
assert.deepEqual(hostedSearchFaviconCandidates("http://example.com/a"), [
"http://example.com/favicon.ico",
]);
assert.ok(
hostedSearchFaviconCandidates("https://news.example.com/c").every(
(src) => !src.includes("favicon.im"),
),
);
assert.deepEqual(hostedSearchFaviconCandidates("file:///tmp/x"), []);
});

7 changes: 6 additions & 1 deletion docs/spec/03-runtime/11-provider-model-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,12 @@ Responses attaches `{ type: "web_search" }`. Chat Completions stays off
except xAI (`vendorKey` xai / `api.x.ai`), which attaches
`search_parameters`. The Composer globe writes `nativeWebSearchEnabled`
(default off). Search results render as a hostedSearch activity row on
the assistant turn and can be expanded to show sources.
the assistant turn and can be expanded to show sources. Inline citation
badges replace a markdown link only when its href is `#cite=N` or matches
a source URL by host and path; a same-host different-path link stays an
ordinary hyperlink. Source favicons load only from that origin's
`/favicon.ico`; the renderer must not send source hostnames to a
third-party favicon service.


This is the **universal escape hatch** guaranteeing market coverage beyond native integrations.
Expand Down
18 changes: 18 additions & 0 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -13140,6 +13140,24 @@ plugin-form fixtures in an isolated temporary directory at runtime.
- **Specs:** 04-ux/06-settings-ia, 04-ux/08-component-spec,
04-ux/09-interaction-patterns; ADR turn-process-and-thinking-display.

### E2E-CHAT-hosted-search-citations-stay-local

- **Preconditions:** An assistant turn with `hostedSearch.sources` that include
`https://example.com/a`, plus markdown that contains `#cite=1`,
`[Read here](https://example.com)`, and `[Same path](https://example.com/a)`.
- **Steps:** Render the turn. Inspect citation badges, remaining hyperlinks,
and favicon image URLs (including failed loads).
- **Expected:** `#cite=1` and the same-path source URL render as citation
badges. The same-host different-path `https://example.com` link stays an
ordinary hyperlink with its original text. Favicon requests go only to each
source origin's `/favicon.ico`; no third-party favicon host is contacted.
- **Automation:** `apps/desktop/test/hosted-search-ui.test.mjs`.
- **Specs:** `03-runtime/11-provider-model-system.md` §16.
- **Acceptance:** Security + C (sessions/transcript).
- **Milestone:** M6+.
- **Status:** Automated unit coverage; renderer E2E still draft.


### E2E-RPC-unicode-separators

- **Preconditions:** Built host-core, shared package and agent runtime; isolated
Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/spec/03-runtime/11-provider-model-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ UI 可能会显示层级提示,但默认情况下不得硬阻止未知模型

目录条目还可以额外固定模型级 wire API(例如 `api: "openai-responses"`)。存在时它优先于 provider 级 `apiStyle`,因此 `opencode_go` 下的 responses-only 模型会走 Responses adapter 而非 Chat Completions;没有模型级固定时保持 provider 级风格不变。

Composer 的联网搜索开关按**解析后的线路 API**决定能否挂厂商托管搜索工具,而不是服务显示名或模型 ID:`anthropic_messages` 附加 `web_search_20250305`,Responses 附加 `{ type: "web_search" }`。`chat_completions` 默认不挂,xAI(`vendorKey` xai / `api.x.ai`)除外,走 `search_parameters`。开关写入设置 `nativeWebSearchEnabled`(缺省关闭)。搜索结果作为助手回合的 hostedSearch 活动行展示,可展开看来源。
Composer 的联网搜索开关按**解析后的线路 API**决定能否挂厂商托管搜索工具,而不是服务显示名或模型 ID:`anthropic_messages` 附加 `web_search_20250305`,Responses 附加 `{ type: "web_search" }`。`chat_completions` 默认不挂,xAI(`vendorKey` xai / `api.x.ai`)除外,走 `search_parameters`。开关写入设置 `nativeWebSearchEnabled`(缺省关闭)。搜索结果作为助手回合的 hostedSearch 活动行展示,可展开看来源。正文超链接仅在 `#cite=N` 或与来源 URL 主机+路径匹配时替换为引用徽章;同主机不同路径保持普通链接。来源 favicon 只从该 origin 的 `/favicon.ico` 加载,不得把来源 hostname 发给第三方 favicon 服务。


### 16.1 Responses 流终止(pi-ai 补丁)
Expand Down
Loading