Skip to content

Commit 5246037

Browse files
committed
feat(omnibar): implement tab and PDF file attachment feature in New Tab Page
- Introduced a new `TASK_SPEC.md` detailing the functionality for attaching open tabs and PDF files to Duck.ai chats from the New Tab Page omnibar. - Updated the omnibar strings to remove outdated loading and error messages related to tab attachment, streamlining user feedback. - Refactored the `AttachmentChips` component to handle tab and file attachments more effectively, ensuring a consistent user experience. - Enhanced the `useFileAttachments` hook to manage file types dynamically based on model support, improving attachment handling. - Removed deprecated image upload components and styles to simplify the attachment interface. These changes significantly enhance the omnibar's capabilities, allowing users to attach relevant context to their chats seamlessly.
1 parent bac8ae1 commit 5246037

9 files changed

Lines changed: 169 additions & 130 deletions

File tree

TASK_SPEC.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Attach Tabs & Files in New Tab Page
2+
3+
| | |
4+
|---|---|
5+
| **Feature** | Attach one or more open tabs (and PDF files) to a Duck.ai chat from the New Tab Page omnibar |
6+
| **Feature flag** | `enableAttachTabs` (tab attachment) · PDF attachment gated per-model by `supportedFileTypes` |
7+
| **Platform** | Desktop — macOS (primary), Windows (native bridge in progress) |
8+
| **Design** | [Figma: Desktop – Attach tabs (All inputs)](https://www.figma.com/design/c1D1uEZt217Y8Zbk2fcd16/Desktop---Attach-tabs--All-inputs-?node-id=1-877&m=dev) |
9+
| **Asana** | [Frontend: Attach 1 or more tabs in New Tab Page](https://app.asana.com/1/137249556945/project/72649045549333/task/1214227868283289) |
10+
11+
## What Is This?
12+
13+
The New Tab Page (NTP) omnibar already lets you start a Duck.ai chat from a fresh
14+
tab. This feature adds a way to bring extra context into that chat **before you
15+
send it**:
16+
17+
- **Attach open tabs** — pick one or more of your currently open browser tabs,
18+
and their page content rides along with your prompt so Duck.ai can answer about
19+
them (e.g. "compare these two laptops").
20+
- **Attach PDF files** — attach a PDF from your computer the same way, for models
21+
that support file input.
22+
23+
Attachments show up as removable **chips** above the omnibar input. You can add
24+
several, remove any of them, and then submit the chat with everything attached.
25+
26+
This brings the NTP omnibar in line with the other places Duck.ai already lives
27+
(the sidebar and the in-browser omnibar), so the attach experience feels the same
28+
everywhere.
29+
30+
## Why?
31+
32+
- As Duck.ai grows from single-page help toward multi-source tasks (comparison,
33+
synthesis, planning), the "current tab only" model is too limiting.
34+
- Users already think of their open tabs as a working set — this gives them a
35+
first-class way to express "use these."
36+
- Shipping it now establishes a consistent foundation across all desktop entry
37+
points before usage scales further.
38+
39+
## User Flows
40+
41+
### Flow A — Attach tabs via the paperclip menu
42+
43+
1. User opens a New Tab Page and focuses the omnibar in AI/Duck.ai mode.
44+
2. User clicks the **paperclip** entry point.
45+
3. A picker opens listing the user's open tabs (most recent first; the NTP tab
46+
itself is not listed).
47+
4. User selects a tab.
48+
5. The page content for that tab is extracted and a **chip** (title + favicon)
49+
appears above the input.
50+
6. User repeats to attach more tabs.
51+
7. User types a prompt and submits — the chat is sent with all attached tab
52+
contexts.
53+
54+
### Flow B — Attach tabs via `@` mention
55+
56+
1. In the omnibar, user types the `@` character.
57+
2. A typeahead appears, filtering the same open-tab list as the user keeps typing.
58+
3. User picks a tab from the typeahead.
59+
4. A chip is added (same as Flow A), and the user continues their prompt and
60+
submits.
61+
62+
### Flow C — Attach a PDF file
63+
64+
1. User clicks the paperclip entry point (shown only when the active model accepts
65+
files).
66+
2. User chooses a PDF from the system file picker.
67+
3. A chip with the file name appears above the input.
68+
4. User submits — the chat is sent with the PDF attached.
69+
70+
### Flow D — Remove an attachment
71+
72+
1. User clicks the remove control on any chip (tab or file).
73+
2. The chip disappears and that context is dropped from the next submit.
74+
75+
## Key Behaviors
76+
77+
| Behavior | Expected outcome |
78+
|---|---|
79+
| Open the picker (paperclip or `@`) | Shows the list of open tabs, most-recently-used first |
80+
| The current NTP tab | Never appears in the tab list |
81+
| `@` typeahead | Filters the same tab list as the paperclip picker, on the client |
82+
| Select a tab | Extracts that tab's page content and renders a chip |
83+
| Submit with N tab chips | All N tab contexts are sent with the chat |
84+
| Select a PDF | Reads + encodes the file in the page and renders a named chip |
85+
| Submit with a PDF chip | The PDF is sent with the chat |
86+
| Remove a chip | That attachment is dropped from the next submit |
87+
| Multiple attachments | Tabs and files can be combined in a single submit |
88+
89+
## Edge Cases & Error Handling
90+
91+
| Scenario | Expected behavior |
92+
|---|---|
93+
| `enableAttachTabs` is off / absent | Paperclip and `@` typeahead for tabs are hidden; existing omnibar flows are unchanged |
94+
| Active model does not accept files | The file/paperclip entry for PDFs is hidden (driven by the model's `supportedFileTypes`) |
95+
| Selected tab is closed, restricted, or content can't be extracted | No content is returned for that tab; it does not produce a usable chip |
96+
| User switches to a model that can't handle an already-attached file type | The unsupported file attachment is cleared |
97+
| Switch between browser tabs while NTP is open | Attached chips persist (per-tab state) |
98+
| NTP tab is closed | Attached chips are cleared |
99+
| Submit with no attachments | Behaves exactly as today — no attachment data is added to the chat |
100+
101+
## Platform Support
102+
103+
| Platform | Status |
104+
|---|---|
105+
| Frontend (NTP web layer) | In scope — picker UI, chips, `@` typeahead, file read/encode |
106+
| macOS (native bridge) | In scope |
107+
| Windows (native bridge) | Planned / in progress |
108+
| iOS / Android | Not in scope (desktop NTP feature) |
109+
110+
## What's NOT in Scope (v1)
111+
112+
- Server-side / native-side tab filtering or search — all filtering (including the
113+
`@` typeahead) happens client-side over the returned tab list.
114+
- File types other than **PDF** — v1 only sends PDFs, though the design leaves room
115+
to add more types later.
116+
- A master on/off switch for PDF support — file attachment is enabled per-model, not
117+
via a single global flag.
118+
- Image attachment changes — images already work today and are unchanged here.
119+
120+
## Testing Notes
121+
122+
**Tabs (frontend):**
123+
- Picker populates the tab list on open.
124+
- `@`-typeahead filters the same list client-side.
125+
- Selecting a tab extracts content and renders a chip.
126+
- Removing a chip drops that context from the next submit.
127+
- A closed/broken tab yields no usable content.
128+
- Submitting with N chips includes N tab contexts in the submitted chat.
129+
- With `enableAttachTabs` absent/false, the paperclip and `@` typeahead are hidden
130+
and existing flows are unchanged.
131+
- Per-tab state: chips persist across browser-tab switches and clear when the NTP
132+
tab closes.
133+
134+
**Files (frontend):**
135+
- The paperclip entry and file-picker `accept` are driven by the active model's
136+
supported file types; the entry is hidden when none are supported.
137+
- Selecting a file adds a chip; removing it drops the file from the next submit.
138+
- The submitted chat carries files matching the attached chips.
139+
- Switching to a model that doesn't support an attached file's type clears it.
140+
141+
**Mock mode / dev tools:**
142+
- Testable through the existing NTP `mock-transport.js`.
143+
- Enable tab attachment with the query param `?omnibar.enableAttachTabs=true`.
144+
- Mock transport returns a set of mock tabs with a simulated extraction delay.
145+
- For files, any mock model declaring PDF support surfaces the file entry point.
146+
147+
## Open Questions
148+
149+
- **AL1** — Should the native layer enforce a size/count cap on attached files
150+
before forwarding to Duck.ai, and if so what limit? (Raised in the PDF tech
151+
design; unresolved.)
152+
153+
## Analytics (TBD)
154+
155+
No pixels/events are defined in the source tasks. The following are candidates that
156+
should be specified before launch:
157+
158+
- Paperclip entry point opened.
159+
- Tab attached (via paperclip vs. `@` mention).
160+
- File (PDF) attached.
161+
- Attachment removed.
162+
- Chat submitted with N tab attachments / with a file attachment.

special-pages/pages/new-tab/app/omnibar/components/chat-tools/attachments/AttachmentChips.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ export function AttachmentChips({ tabs, files, images, onRemoveTab, onRemoveFile
8080
removeLabel={t('omnibar_removeImageLabel')}
8181
/>
8282
);
83-
default:
84-
return null;
83+
default: {
84+
/** @type {never} */
85+
const _exhaustiveCheck = item;
86+
return _exhaustiveCheck;
87+
}
8588
}
8689
})}
8790
</div>

special-pages/pages/new-tab/app/omnibar/components/chat-tools/file-attachment/useFileAttachments.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ const FILE_READ_TIMEOUT = 30000;
1212
*/
1313
export function useFileAttachments(supportedFileTypes) {
1414
const [attachedFiles, setAttachedFiles] = useState(/** @type {AttachedFile[]} */ ([]));
15-
1615
const allowList = supportedFileTypes ?? [];
1716
const allowListKey = allowList.join('|');
1817

19-
// Drop attachments whose MIME isn't in the current model's allow-list.
20-
// In-render reset (React's sync-state-from-props pattern) so the
21-
// submitted set always matches the active model.
2218
const [prevAllowListKey, setPrevAllowListKey] = useState(allowListKey);
2319
if (prevAllowListKey !== allowListKey) {
2420
setPrevAllowListKey(allowListKey);

special-pages/pages/new-tab/app/omnibar/components/chat-tools/image-attachment/ImageAttachmentTool.js

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Fragment, h } from 'preact';
22
import { useLayoutEffect, useRef } from 'preact/hooks';
33
import { useTypedTranslationWith } from '../../../../types';
44
import { MAX_IMAGES, getImageErrorMessage } from './useImageAttachments';
5-
import { ImageUploadButton as ImageUploadButtonUI } from './ImageUploadButton';
6-
import { Tooltip } from '../../Tooltip.js';
75
import styles from './ImageAttachment.module.css';
86

97
/**
@@ -76,30 +74,3 @@ export function ImageAttachmentContent({ state, supportsImageUpload, onVisibleIm
7674
</>
7775
);
7876
}
79-
80-
/**
81-
* Toolbar button for image uploads. Renders the upload button with
82-
* a tooltip when the image limit is reached. Place in the form's leftSlot.
83-
*
84-
* @param {object} props
85-
* @param {ImageAttachmentState} props.state
86-
*/
87-
export function ImageUploadButton({ state }) {
88-
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
89-
const { imageUploadDisabled, handleFileChange } = state;
90-
91-
const imageLimitWarning = t('omnibar_imageAttachmentLimitWarning', { limit: String(MAX_IMAGES) });
92-
const uploadButton = (
93-
<ImageUploadButtonUI disabled={imageUploadDisabled} onChange={handleFileChange} ariaLabel={t('omnibar_attachImageLabel')} />
94-
);
95-
96-
if (imageUploadDisabled) {
97-
return (
98-
<Tooltip content={imageLimitWarning} position="above">
99-
{uploadButton}
100-
</Tooltip>
101-
);
102-
}
103-
104-
return uploadButton;
105-
}

special-pages/pages/new-tab/app/omnibar/components/chat-tools/image-attachment/ImageUploadButton.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

special-pages/pages/new-tab/app/omnibar/components/chat-tools/tab-attachment/AttachMenu.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Fragment, h } from 'preact';
22
import { useRef, useState } from 'preact/hooks';
3+
import cn from 'classnames';
34
import { useTypedTranslationWith } from '../../../../types';
45
import { ChevronSmall, FolderIcon, PageContentIcon, PaperclipIcon } from '../../../../components/Icons';
56
import { useDropdown } from '../useDropdown';
@@ -70,7 +71,7 @@ function DirectFileButton({ ariaLabel, accept, disabled, onChange }) {
7071

7172
return (
7273
<label
73-
class={disabled ? `${imageStyles.toolButton} ${imageStyles.toolButtonDisabled}` : imageStyles.toolButton}
74+
class={cn(imageStyles.toolButton, { [imageStyles.toolButtonDisabled]: disabled })}
7475
aria-label={ariaLabel}
7576
aria-disabled={disabled}
7677
role="button"

special-pages/pages/new-tab/app/omnibar/components/chat-tools/tab-attachment/MentionPicker.module.css

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,6 @@
6060
color: var(--ds-color-theme-accent-content-primary);
6161
}
6262

63-
.rowAttached {
64-
color: var(--ds-color-theme-text-secondary);
65-
}
66-
67-
.rowActive.rowAttached {
68-
color: var(--ds-color-theme-accent-content-primary);
69-
}
70-
7163
.favicon {
7264
border-radius: 4px;
7365
flex: none;
@@ -97,17 +89,3 @@
9789
text-overflow: ellipsis;
9890
white-space: nowrap;
9991
}
100-
101-
.attachedMark {
102-
flex: none;
103-
font-size: 12px;
104-
font-weight: 700;
105-
line-height: 1;
106-
}
107-
108-
.statusMessage {
109-
color: var(--ds-color-theme-text-secondary);
110-
font-size: 13px;
111-
margin: 0;
112-
padding: var(--sp-2) var(--sp-3) var(--sp-3);
113-
}

special-pages/pages/new-tab/app/omnibar/strings.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,6 @@
195195
"title": "Pick an open tab to attach",
196196
"description": "Accessible label for the open-tabs picker panel (the dialog wrapper around the listbox)."
197197
},
198-
"omnibar_attachTabsLoading": {
199-
"title": "Loading open tabs…",
200-
"description": "Loading state shown while the open-tabs picker is fetching the tab list from native."
201-
},
202-
"omnibar_attachTabsError": {
203-
"title": "Couldn't load open tabs. Try again.",
204-
"description": "Error state shown when the open-tabs picker fails to fetch the tab list."
205-
},
206-
"omnibar_attachTabsEmpty": {
207-
"title": "No matching tabs",
208-
"description": "Empty-state message shown in the open-tabs picker when no tabs match the current filter."
209-
},
210198
"omnibar_removeAttachedTabLabel": {
211199
"title": "Remove {title}",
212200
"description": "Accessible label for the close button on an attached-tab chip. {title} is the tab title."

special-pages/pages/new-tab/public/locales/en/new-tab.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -357,18 +357,6 @@
357357
"title": "Pick an open tab to attach",
358358
"description": "Accessible label for the open-tabs picker panel (the dialog wrapper around the listbox)."
359359
},
360-
"omnibar_attachTabsLoading": {
361-
"title": "Loading open tabs…",
362-
"description": "Loading state shown while the open-tabs picker is fetching the tab list from native."
363-
},
364-
"omnibar_attachTabsError": {
365-
"title": "Couldn't load open tabs. Try again.",
366-
"description": "Error state shown when the open-tabs picker fails to fetch the tab list."
367-
},
368-
"omnibar_attachTabsEmpty": {
369-
"title": "No matching tabs",
370-
"description": "Empty-state message shown in the open-tabs picker when no tabs match the current filter."
371-
},
372360
"omnibar_removeAttachedTabLabel": {
373361
"title": "Remove {title}",
374362
"description": "Accessible label for the close button on an attached-tab chip. {title} is the tab title."

0 commit comments

Comments
 (0)