Conversation
There was a problem hiding this comment.
Hey - 我发现了两个问题,并补充了一些整体性的反馈:
TaskList和TabBar里的导出处理逻辑重复使用了相同的exportTabConfig+ toast 逻辑;可以考虑抽取一个共享的辅助函数,以保持行为一致并减少后期维护成本。App中的Toaster组件在两个布局分支里各渲染了一次,且传入的 props 完全相同;你可以把它上移到一个共享的位置,从而避免重复。exportTabConfig和importTabConfigFromClipboard都假设navigator.clipboard、CompressionStream和DecompressionStream一定可用;建议增加能力检查或更清晰的回退错误提示,以提高在不支持环境中的健壮性。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- The export handlers in `TaskList` and `TabBar` duplicate the same `exportTabConfig` + toast logic; consider extracting a shared helper to keep behavior consistent and reduce maintenance overhead.
- The `Toaster` component is rendered twice in `App` (for the two layout branches) with identical props; you could hoist this into a single shared location to avoid duplication.
- Both `exportTabConfig` and `importTabConfigFromClipboard` assume `navigator.clipboard`, `CompressionStream`, and `DecompressionStream` are available; consider adding capability checks or a clearer fallback error to improve robustness in unsupported environments.
## Individual Comments
### Comment 1
<location path="src/components/TaskList.tsx" line_range="321" />
<code_context>
+ tabName: instance.name,
+ });
+ const footer = t('preset.exportShareFooter', { projectName });
+ exportTabConfig(instance, projectName, hint, footer).then(
+ () => toast.success(t('preset.exportSuccess')),
+ () => toast.error(t('preset.importFailed')),
</code_context>
<issue_to_address>
**issue (bug_risk):** Export failure toast is using the import error message key, which will show misleading text.
The failure handler is reusing `t('preset.importFailed')`, whose text is specific to imports. That will be confusing when an export fails. Please add a dedicated `preset.exportFailed` key or reuse a generic error string that correctly describes an export failure.
</issue_to_address>
### Comment 2
<location path="src/utils/tabExportImport.ts" line_range="149-153" />
<code_context>
+ };
+}
+
+// ── gzip helpers ─────────────────────────────────────────────────────────────
+
+async function compress(str: string): Promise<Uint8Array> {
+ const encoded = new TextEncoder().encode(str);
+ const cs = new CompressionStream('deflate-raw');
+ const writer = cs.writable.getWriter();
+ writer.write(encoded);
</code_context>
<issue_to_address>
**nitpick:** Helper section is labeled as gzip while using `deflate-raw`, which may cause confusion for future maintenance.
The helpers and section header are named "gzip", but they use `CompressionStream('deflate-raw')` / `DecompressionStream('deflate-raw')`. Since gzip and raw deflate are different formats, this may mislead anyone expecting gzip interoperability. Consider renaming the section (and helper names if needed) to "deflate" to better reflect the behavior.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的 Review。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The export handlers in
TaskListandTabBarduplicate the sameexportTabConfig+ toast logic; consider extracting a shared helper to keep behavior consistent and reduce maintenance overhead. - The
Toastercomponent is rendered twice inApp(for the two layout branches) with identical props; you could hoist this into a single shared location to avoid duplication. - Both
exportTabConfigandimportTabConfigFromClipboardassumenavigator.clipboard,CompressionStream, andDecompressionStreamare available; consider adding capability checks or a clearer fallback error to improve robustness in unsupported environments.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The export handlers in `TaskList` and `TabBar` duplicate the same `exportTabConfig` + toast logic; consider extracting a shared helper to keep behavior consistent and reduce maintenance overhead.
- The `Toaster` component is rendered twice in `App` (for the two layout branches) with identical props; you could hoist this into a single shared location to avoid duplication.
- Both `exportTabConfig` and `importTabConfigFromClipboard` assume `navigator.clipboard`, `CompressionStream`, and `DecompressionStream` are available; consider adding capability checks or a clearer fallback error to improve robustness in unsupported environments.
## Individual Comments
### Comment 1
<location path="src/components/TaskList.tsx" line_range="321" />
<code_context>
+ tabName: instance.name,
+ });
+ const footer = t('preset.exportShareFooter', { projectName });
+ exportTabConfig(instance, projectName, hint, footer).then(
+ () => toast.success(t('preset.exportSuccess')),
+ () => toast.error(t('preset.importFailed')),
</code_context>
<issue_to_address>
**issue (bug_risk):** Export failure toast is using the import error message key, which will show misleading text.
The failure handler is reusing `t('preset.importFailed')`, whose text is specific to imports. That will be confusing when an export fails. Please add a dedicated `preset.exportFailed` key or reuse a generic error string that correctly describes an export failure.
</issue_to_address>
### Comment 2
<location path="src/utils/tabExportImport.ts" line_range="149-153" />
<code_context>
+ };
+}
+
+// ── gzip helpers ─────────────────────────────────────────────────────────────
+
+async function compress(str: string): Promise<Uint8Array> {
+ const encoded = new TextEncoder().encode(str);
+ const cs = new CompressionStream('deflate-raw');
+ const writer = cs.writable.getWriter();
+ writer.write(encoded);
</code_context>
<issue_to_address>
**nitpick:** Helper section is labeled as gzip while using `deflate-raw`, which may cause confusion for future maintenance.
The helpers and section header are named "gzip", but they use `CompressionStream('deflate-raw')` / `DecompressionStream('deflate-raw')`. Since gzip and raw deflate are different formats, this may mislead anyone expecting gzip interoperability. Consider renaming the section (and helper names if needed) to "deflate" to better reflect the behavior.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull request overview
该 PR 新增“Tab 配置导入/导出(剪贴板分享)”能力,并在 UI 中提供入口与提示反馈(toast)。
Changes:
- 新增
tabExportImport工具:将 Tab 配置压缩编码写入剪贴板,并支持从剪贴板解析导入 - 在任务列表与标签页右键菜单中加入“导入/导出配置”入口,并用 toast 提示结果
- 补齐多语言文案,并引入
sonner作为 toast 组件(App 内挂载 Toaster)
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/tabExportImport.ts | 新增导入/导出协议、压缩编码与解析逻辑 |
| src/components/TaskList.tsx | 空任务页增加“从剪贴板导入”,任务列表右键菜单增加“导出配置” |
| src/components/TabBar.tsx | Tab 右键菜单增加“导出配置” |
| src/App.tsx | 挂载 sonner 的 <Toaster /> 以显示 toast |
| src/i18n/locales/*.ts | 新增导入/导出相关文案与菜单项翻译 |
| package.json / pnpm-lock.yaml | 增加 sonner 依赖 |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| let binary = ''; | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| binary += String.fromCharCode(bytes[i]); | ||
| } |
There was a problem hiding this comment.
toBase64Url builds a large string via binary += ... in a loop, which can become quadratic and memory-heavy for bigger payloads. Consider using chunked String.fromCharCode (or a dedicated base64url encoder) to avoid performance degradation when exporting large task sets.
| let binary = ''; | |
| for (let i = 0; i < bytes.length; i++) { | |
| binary += String.fromCharCode(bytes[i]); | |
| } | |
| const chunkSize = 0x8000; | |
| const binaryChunks: string[] = []; | |
| for (let i = 0; i < bytes.length; i += chunkSize) { | |
| const chunk = bytes.subarray(i, i + chunkSize); | |
| binaryChunks.push(String.fromCharCode(...chunk)); | |
| } | |
| const binary = binaryChunks.join(''); |
| const footer = t('preset.exportShareFooter', { projectName }); | ||
| exportTabConfig(instance, projectName, hint, footer).then( | ||
| () => toast.success(t('preset.exportSuccess')), | ||
| () => toast.error(t('preset.importFailed')), |
There was a problem hiding this comment.
On export failure this shows preset.importFailed ("Import failed...") which is misleading for an export operation. Add a dedicated export-failure message (e.g., preset.exportFailed) or use a neutral clipboard error string here.
| () => toast.error(t('preset.importFailed')), | |
| () => toast.error(t('preset.exportFailed', 'Export failed')), |
| const footer = t('preset.exportShareFooter', { projectName }); | ||
| exportTabConfig(inst, projectName, hint, footer).then( | ||
| () => toast.success(t('preset.exportSuccess')), | ||
| () => toast.error(t('preset.importFailed')), |
There was a problem hiding this comment.
On export failure this uses preset.importFailed ("Import failed...") which is misleading for an export action. Introduce an export-specific failure message (or a neutral clipboard error) so users get the correct feedback.
| () => toast.error(t('preset.importFailed')), | |
| () => | |
| toast.error( | |
| t('preset.exportFailed', { defaultValue: 'Export failed' }), | |
| ), |
| const inst = instances.find((i) => i.id === instanceId); | ||
| const projectName = projectInterface?.name; | ||
| if (inst && projectName) { | ||
| const hint = t('preset.exportShareHint', { |
There was a problem hiding this comment.
This context-menu handler is wrapped in useCallback, but it now reads projectInterface?.name (and uses other store values) without those being represented in the callback’s dependency list. That can create stale closures (e.g., exporting with an outdated project name). Update the dependencies (or remove useCallback if memoization isn’t needed) to ensure the export uses current state.
| case 's': return { type: 'select', caseName: w.c }; | ||
| case 'cb': return { type: 'checkbox', caseNames: w.c }; | ||
| case 'sw': return { type: 'switch', value: w.v }; | ||
| case 'in': return { type: 'input', values: w.v }; |
There was a problem hiding this comment.
decodeOptionValue lacks a fallback for unexpected w.t values; for malformed/forged clipboard payloads this can silently produce undefined optionValues instead of failing fast, leading to downstream runtime errors. Add explicit validation (e.g., default branch throwing) so the import is rejected as invalid_format when an unknown tag is encountered.
| case 'in': return { type: 'input', values: w.v }; | |
| case 'in': return { type: 'input', values: w.v }; | |
| default: | |
| throw new Error('invalid_format'); |
| // ── gzip helpers ───────────────────────────────────────────────────────────── | ||
|
|
||
| async function compress(str: string): Promise<Uint8Array> { | ||
| const encoded = new TextEncoder().encode(str); | ||
| const cs = new CompressionStream('deflate-raw'); | ||
| const writer = cs.writable.getWriter(); | ||
| writer.write(encoded); | ||
| writer.close(); |
There was a problem hiding this comment.
Comments and docstrings describe the payload as gzip(JSON), but the implementation uses CompressionStream('deflate-raw')/DecompressionStream('deflate-raw'). Please align the documentation (and the helper section header) with the actual compression format, or switch the implementation to gzip if that’s the intended wire format.
| // 从任意行里提取协议行(忽略前缀说明文字) | ||
| const dataLineMatch = rawText.match(/(.+:\/\/tab-sharing\/.+)/m); | ||
| const text = dataLineMatch ? dataLineMatch[1].trim() : rawText; | ||
|
|
||
| const protocolPrefix = `${projectName}://${PROTOCOL_SEGMENT}/`; | ||
| if (!text.startsWith(protocolPrefix)) { | ||
| if (/^.+:\/\/tab-sharing\//.test(text)) { | ||
| throw createImportError('project_mismatch'); |
There was a problem hiding this comment.
Import parsing hard-codes tab-sharing in regex checks (/tab-sharing/) instead of using PROTOCOL_SEGMENT. If PROTOCOL_SEGMENT ever changes, these checks will drift and break imports. Build the regex/prefix checks from PROTOCOL_SEGMENT (escaping as needed) to keep the wire format consistent.
Summary by Sourcery
为任务和预设添加基于剪贴板的标签页配置导入/导出功能,并提供本地化消息和吐司通知。
New Features:
Enhancements:
Build:
sonner依赖以支持吐司通知功能。Original summary in English
Summary by Sourcery
Add clipboard-based tab configuration import/export for tasks and presets, with localized messaging and toast notifications.
New Features:
Enhancements:
Build: