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
11 changes: 6 additions & 5 deletions apps/desktop/electron/main/plugin-launcher-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { join } from "node:path";
export const PLUGIN_LAUNCHER_STATE_FILE = "plugin-launcher-state.json";

/**
* The launcher must fit eight columns without squeezing: below this width the
* grid drops to six columns, which is the one thing the user cannot undo by
* dragging.
* The launcher opens wide enough for a 48px icon and a readable name in every
* one of its eight columns. Below this floor a tile is about 90px wide and the
* longest names start losing characters, so the window refuses to go narrower
* rather than let the grid squeeze.
*/
export const PLUGIN_LAUNCHER_MIN_WIDTH = 560;
export const PLUGIN_LAUNCHER_MIN_WIDTH = 800;
/**
* Height is not a user preference any more: the renderer measures what it
* renders and asks for exactly that height through
Expand All @@ -30,7 +31,7 @@ export const PLUGIN_LAUNCHER_MIN_CONTENT_HEIGHT = 120;
/** Wider than this the eight tiles stop being scannable. */
export const PLUGIN_LAUNCHER_MAX_WIDTH = 1280;
export const PLUGIN_LAUNCHER_MAX_HEIGHT = 940;
export const PLUGIN_LAUNCHER_DEFAULT_WIDTH = 760;
export const PLUGIN_LAUNCHER_DEFAULT_WIDTH = 900;
/** Initial height before the panel has measured itself. */
export const PLUGIN_LAUNCHER_DEFAULT_HEIGHT = 440;

Expand Down
22 changes: 8 additions & 14 deletions apps/desktop/src/components/launcher/LauncherTileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import type { LauncherTile } from "../../lib/launcher-search";
import { IconPin } from "../icons";

/**
* One grid cell: icon, type badge, and name — plus its own pin toggle.
* One grid cell: a rounded-square icon and the name, plus its own pin toggle.
*
* Every cell has the same three parts so the eight-column grid stays aligned:
* the icon shape (square plugin, round application) and the small type badge
* carry the source, and the name replaces the old second text line, which used
* to explain why the item matched. A long name is ellipsized instead of
* wrapping, which would break the row rhythm.
* Every source renders the same shape at the same size, so the grid reads as
* one set of tools and the whole line below the icon belongs to the name. The
* source still appears in the hover title for anyone who needs it, but it no
* longer costs a corner of the icon or a word of the name. A long name is
* ellipsized instead of wrapping, which would break the row rhythm.
*
* The pin sits in the corner and is only visible while the cell is hovered,
* active or focused; an already pinned cell keeps it visible and filled.
Expand Down Expand Up @@ -71,10 +71,7 @@ export function LauncherTileButton({
title={title}
onClick={onOpen}
>
<span
className={`plugin-launcher-icon plugin-launcher-icon-${tile.kind}`}
aria-hidden
>
<span className="plugin-launcher-icon" aria-hidden>
{showIcon ? (
<img
src={tile.iconDataUrl}
Expand All @@ -86,10 +83,7 @@ export function LauncherTileButton({
<span className="plugin-launcher-monogram">{tile.monogram}</span>
)}
</span>
<span className="plugin-launcher-tile-line">
<span className="plugin-launcher-tile-name">{tile.name}</span>
<span className="plugin-launcher-tile-type">{typeLabel}</span>
</span>
<span className="plugin-launcher-tile-name">{tile.name}</span>
</button>
<button
type="button"
Expand Down
51 changes: 14 additions & 37 deletions apps/desktop/src/styles/plugin-launcher.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,73 +188,50 @@ html[data-surface="plugin-launcher"] #root {
}

/*
* Shape is the fastest source signal: plugins are square, applications are
* round. The type badge beside the name repeats it in words.
* Every tile carries the same rounded square. Plugin panels and installed
* applications used to differ by shape and by a word beside the name; the grid
* now reads as one set of tools, and the whole line below the icon belongs to
* the name. The icon grew to 48px so it can carry the identity on its own.
*/
.plugin-launcher-icon {
width: 34px;
height: 34px;
flex: 0 0 34px;
width: 48px;
height: 48px;
flex: 0 0 48px;
display: grid;
place-items: center;
overflow: hidden;
border-radius: var(--radius-lg);
background: var(--ds-tile-deep);
color: var(--ds-text-primary);
}

.plugin-launcher-icon-plugin {
border-radius: var(--radius-sm);
}

.plugin-launcher-icon-app {
border-radius: var(--radius-full);
}

.plugin-launcher-icon img {
width: 26px;
height: 26px;
width: 34px;
height: 34px;
object-fit: contain;
/** Native icons are already round or square; never stretch them. */
/** A platform icon is already an icon; never stretch it. */
pointer-events: none;
}

.plugin-launcher-monogram {
font-size: var(--text-sm-plus);
font-size: var(--text-xl);
font-weight: var(--font-weight-semibold);
line-height: var(--leading-tighter);
}

/* Name and type badge share one line; the name is the part that truncates. */
.plugin-launcher-tile-line {
width: 100%;
min-width: 0;
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
overflow: hidden;
}

.plugin-launcher-tile-name {
width: 100%;
min-width: 0;
overflow: hidden;
color: inherit;
font-size: var(--text-xs);
font-weight: var(--font-weight-medium-plus);
line-height: var(--leading-compact);
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}

.plugin-launcher-tile-type {
flex: 0 0 auto;
color: var(--ds-text-faint);
font-size: var(--text-2xs, 10px);
font-weight: var(--font-weight-normal);
line-height: var(--leading-compact);
white-space: nowrap;
}

/* The pin is quiet until the cell is under the pointer or focus. */
.plugin-launcher-pin {
position: absolute;
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/test/plugin-launcher-window.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ test("launcher renderer supports keyboard selection and has no window controls",
assert.match(styles, /html\[data-surface="plugin-launcher"\][\s\S]*background: transparent/);
});

test("the launcher grid is eight fixed columns with square plugins and round apps", () => {
test("the launcher grid is eight fixed columns with one tile shape", () => {
assert.match(
styles,
/\.plugin-launcher-grid \{[\s\S]*grid-template-columns: repeat\(8, minmax\(0, 1fr\)\)/,
);
assert.match(styles, /\.plugin-launcher-icon-plugin \{[\s\S]*border-radius: var\(--radius-sm\)/);
assert.match(styles, /\.plugin-launcher-icon-app \{[\s\S]*border-radius: var\(--radius-full\)/);
// One shape for every source: no per-kind icon rule and no type badge.
assert.match(styles, /\.plugin-launcher-icon \{[\s\S]*width: 48px/);
assert.match(styles, /\.plugin-launcher-icon \{[\s\S]*border-radius: var\(--radius-lg\)/);
assert.doesNotMatch(styles, /plugin-launcher-icon-(plugin|app)/);
assert.doesNotMatch(styles, /plugin-launcher-tile-type/);
});

test("the launcher window is resizable and remembers its size", () => {
Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0343-plugin-launcher-local-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ app data directory, not host-core user data.
## Consequences

- One shortcut opens "anything openable" — plugin panels and installed
applications — with a source tag on every tile so the user can tell them
apart even when the names collide.
applications — in one grid that renders every source as the same rounded
square. The trade-off is deliberate: the panel no longer tells a plugin from
an application before the user opens it, which buys a wider name line for
every tile; a shared name still lists both entries rather than one.
- The privilege boundary is a window, not a permission: revoking it means
removing the channel, not editing a plugin manifest. The launcher window runs
with `contextIsolation`, `sandbox`, and no Node integration like every other
Expand Down
8 changes: 6 additions & 2 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -19694,6 +19694,8 @@ their separate platform acceptance.
8. Paste 500 characters of garbage into the search field.
9. Pin an application, quit, restart, and reopen the launcher with an empty
input.
10. Install a plugin whose name matches an installed application (for example
`Calculator`) and open the launcher with an empty input.
- **Expected**: Step 1 shows `Recently used` with two rows of eight tiles and
`Pinned` with one row of eight once the user has pins; the recents header
reports the true number of remembered items and offers `Show all {count} →`
Expand All @@ -19708,8 +19710,10 @@ their separate platform acceptance.
plugin from recents, pins, and matches while every other tile still opens.
Step 8 keeps the panel responsive (no frozen input, no unhandled error) and
reports no match, because input longer than 256 characters is never matched
against a trigger. Step 9 still shows the pinned application in the `Pinned`
row.
row. Step 10 lists both entries with the identical 48px rounded square and the
same name line — the source is only in the hover title — and opening the plugin
row still lands in the plugin panel while the application row launches the
program.
- **Automation**: `pnpm --filter @pi-desktop/desktop test` runs the offline
coverage for these rules: `launcher-search.test.mjs` (ranking order, section
caps, overflow total, trigger whole-input matching, the 256-character limit
Expand Down
33 changes: 16 additions & 17 deletions docs/spec/07-plugins/18-plugin-launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,20 @@ closes the panel, focuses the main window, and opens the plugin market page

## 4. Tiles

Every tile shows the same three parts:
Every tile shows the same two parts:

- the icon — an application's platform icon, or a monogram for a plugin and for
an application whose icon could not be read; an `<img>` that fails to load
falls back to the monogram instead of leaving a hole;
- the name, ellipsized when it does not fit;
- a type badge on the same line as the name — `Plugin` or `App`.
- the icon — 48px, and the same rounded square for every source: an
application's platform icon, or a monogram for a plugin and for an
application whose icon could not be read; an `<img>` that fails to load falls
back to the monogram instead of leaving a hole;
- the name, ellipsized when it does not fit, on a line of its own.

A tile does not explain why it matched: there is no second line, so every tile is
the same height and the eight-column grid stays aligned. The `title` tooltip
carries `name · type` (plus the executable name for an application) and never the
launcher payload.

Plugins render a square tile and applications a circular one. A plugin and an
application that share a name are therefore both listed and visually
distinguishable rather than deduplicated.
A tile does not explain why it matched, and it does not announce what kind of
item it is: plugin panels and installed applications render identically, so the
whole line under the icon belongs to the name and the grid reads as one set of
tools. The `title` tooltip still carries `name · type` (plus the executable name
for an application) and never the launcher payload. A plugin and an application
that share a name are therefore both listed rather than deduplicated.

Each tile carries a pin toggle in its corner (§8) with `aria-pressed` and the
`Pin to panel` / `Unpin` label. It is hidden until the tile is hovered, active,
Expand Down Expand Up @@ -242,9 +240,10 @@ ADR [0343](../../adr/0343-plugin-launcher-local-apps.md) records this decision.

## 10. Window size, position, and state

- The window is resizable between 560 and 1280 DIP wide; the minimum width is the
smallest width at which eight columns still fit without squeezing, and the
maximum keeps eight tiles scannable.
- The window is resizable between 800 and 1280 DIP wide and opens at 900. The
floor is the narrowest width at which an eight-column row still leaves a 48px
icon and a readable name in every cell; the maximum keeps eight tiles
scannable.
- Height always fits the content. The renderer measures the search row, the
natural height of the results content, the footer, and the panel padding and
reports the total through `pi-desktop/pluginLauncher/contentHeight`; the host
Expand Down
13 changes: 6 additions & 7 deletions docs/zh-CN/spec/07-plugins/18-plugin-launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,14 @@

## 4. 磁贴

每个磁贴都只有同样的三部分:
每个磁贴都只有同样的两部分:

- 图标——应用使用平台图标;插件、以及读不到图标的应用使用首字母色块;`<img>` 加载失败也回退到色块,不会留下空洞;
- 名称,放不下时以省略号截断;
- 与名称同一行的类型徽标——`Plugin` 或 `App`。
- 图标——48px,所有来源统一为圆角方形:应用使用平台图标,插件、以及读不到图标的应用使用首字母色块;`<img>` 加载失败也回退到色块,不会留下空洞;
- 名称独占图标下方的一整行,放不下时省略。

磁贴不再解释命中的原因:没有第二行,因此每个磁贴高度一致,八列网格保持对齐。`title` 提示为 `名称 · 类型`(应用还会附上可执行文件名),绝不包含启动载荷。
磁贴不再解释命中原因,也不再说明条目类型:插件面板与已安装应用外观完全一致,图标下方整行都留给名称,网格读起来就是一套工具。`title` 提示仍为 `名称 · 类型`(应用还会附上可执行文件名),绝不包含启动载荷。

插件使用方形磁贴,应用使用圆形磁贴。因此同名插件与应用会同时列出并且在视觉上可区分,而不是被去重。
同名插件与应用仍会同时列出,而不是被去重——只是不再靠形状或徽标区分。

每个磁贴右上角有一个图钉,带 `aria-pressed` 与 `Pin to panel` / `Unpin` 标签。它只在悬停、激活或聚焦时显示,已固定的磁贴常显,且不进入 Tab 序列——面板只由搜索框驱动。

Expand Down Expand Up @@ -142,7 +141,7 @@

## 10. 窗口尺寸、位置与状态

- 窗口宽度可在 560 与 1280 DIP 之间缩放;最小宽度是八列仍能完整排布的最小宽度,最大值保证八列磁贴仍可扫读。
- 窗口宽度可在 800 与 1280 DIP 之间缩放,默认打开 900;下限保证八列里每格仍放得下 48px 图标与可读名称,上限保证八列磁贴仍可扫读。
- 高度始终贴合内容。渲染端测量搜索行、结果内容的自然高度、底部栏以及面板内边距,并通过 `pi-desktop/pluginLauncher/contentHeight` 上报总高度;主进程将其钳制在 120–940 DIP 后设置内容尺寸,因此面板既不会带着空白打开,也不会裁掉一行。
- 宽度与位置持久化在 `<data dir>/plugin-launcher-state.json`,由主进程尽力写入并做防抖(`apps/desktop/electron/main/plugin-launcher-state.ts`)。高度不是用户偏好:记住的高度只是首次测量到达前的起点。
- 只有整个面板仍能落在即将打开的工作区内时,才会采用记住的位置;否则在該工作区重新居中。
Expand Down
Loading