Skip to content

feat: 新的导入导出功能#178

Merged
MistEO merged 3 commits intomainfrom
feat/export
Apr 13, 2026
Merged

feat: 新的导入导出功能#178
MistEO merged 3 commits intomainfrom
feat/export

Conversation

@MistEO
Copy link
Copy Markdown
Owner

@MistEO MistEO commented Apr 13, 2026

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:

  • Allow exporting the current tab's task configuration to a compressed, shareable clipboard string via context menus in the task list and tab bar.
  • Allow importing task configurations from the clipboard into a tab, automatically applying tasks, controller/resource selection, and pre-actions.

Enhancements:

  • Show inline import buttons in empty task states and preset selector to streamline starting from shared configurations.
  • Add localized preset import/export labels, hints, and error messages across supported languages.
  • Introduce consistent bottom-center toast notifications styled to match the app theme for import/export feedback.

Build:

  • Add the sonner dependency for toast notification support.

Copilot AI review requested due to automatic review settings April 13, 2026 16:25
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并补充了一些整体性的反馈:

  • TaskListTabBar 里的导出处理逻辑重复使用了相同的 exportTabConfig + toast 逻辑;可以考虑抽取一个共享的辅助函数,以保持行为一致并减少后期维护成本。
  • App 中的 Toaster 组件在两个布局分支里各渲染了一次,且传入的 props 完全相同;你可以把它上移到一个共享的位置,从而避免重复。
  • exportTabConfigimportTabConfigFromClipboard 都假设 navigator.clipboardCompressionStreamDecompressionStream 一定可用;建议增加能力检查或更清晰的回退错误提示,以提高在不支持环境中的健壮性。
给 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>

Sourcery 对开源项目免费——如果你觉得这些 Review 有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的 Review。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/components/TaskList.tsx Outdated
Comment thread src/utils/tabExportImport.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/utils/tabExportImport.ts Outdated
Comment on lines +197 to +200
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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('');

Copilot uses AI. Check for mistakes.
Comment thread src/components/TaskList.tsx Outdated
const footer = t('preset.exportShareFooter', { projectName });
exportTabConfig(instance, projectName, hint, footer).then(
() => toast.success(t('preset.exportSuccess')),
() => toast.error(t('preset.importFailed')),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
() => toast.error(t('preset.importFailed')),
() => toast.error(t('preset.exportFailed', 'Export failed')),

Copilot uses AI. Check for mistakes.
Comment thread src/components/TabBar.tsx Outdated
const footer = t('preset.exportShareFooter', { projectName });
exportTabConfig(inst, projectName, hint, footer).then(
() => toast.success(t('preset.exportSuccess')),
() => toast.error(t('preset.importFailed')),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
() => toast.error(t('preset.importFailed')),
() =>
toast.error(
t('preset.exportFailed', { defaultValue: 'Export failed' }),
),

Copilot uses AI. Check for mistakes.
Comment thread src/components/TabBar.tsx Outdated
Comment on lines +188 to +191
const inst = instances.find((i) => i.id === instanceId);
const projectName = projectInterface?.name;
if (inst && projectName) {
const hint = t('preset.exportShareHint', {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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 };
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
case 'in': return { type: 'input', values: w.v };
case 'in': return { type: 'input', values: w.v };
default:
throw new Error('invalid_format');

Copilot uses AI. Check for mistakes.
Comment thread src/utils/tabExportImport.ts Outdated
Comment on lines +149 to +156
// ── 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();
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/tabExportImport.ts Outdated
Comment on lines +265 to +272
// 从任意行里提取协议行(忽略前缀说明文字)
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');
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@MistEO MistEO merged commit 7209435 into main Apr 13, 2026
3 checks passed
@MistEO MistEO deleted the feat/export branch April 13, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants