dsh-image-gen 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared browser image and DOM helpers for client views.
3
+ * Completely decoupled from gallery-view and studio-view to prevent circular dependencies.
4
+ */
5
+ /** Copy an image Blob to the OS clipboard, converting to PNG if required by the browser. */
6
+ export declare function copyImageBlob(blob: Blob): Promise<boolean>;
7
+ /** Safely trigger a browser file download by mounting an anchor element into the DOM. */
8
+ export declare function downloadBlobUrl(url: string, filename: string): void;
9
+ /** Human-readable relative time formatter supporting zh and en. */
10
+ export declare function formatRelativeTime(timestamp: number, lang: 'zh' | 'en'): string;
@@ -0,0 +1,25 @@
1
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
2
+ import type { CloudImageProvider } from '../shared.js';
3
+ /** One regenerated version displayed in place of an original conversation image. */
4
+ export interface ConversationImageRevision {
5
+ attachment: ImageAttachmentRef;
6
+ prompt: string;
7
+ provider: CloudImageProvider;
8
+ model: string;
9
+ output: string;
10
+ createdAt: number;
11
+ ratio: string;
12
+ quality: string;
13
+ }
14
+ export interface ConversationImageRevisionChain {
15
+ originId: string;
16
+ /** Zero selects the immutable conversation image; revisions use one-based positions. */
17
+ currentIndex: number;
18
+ revisions: ConversationImageRevision[];
19
+ }
20
+ /** Read the locally persisted replacement history for one immutable conversation image. */
21
+ export declare function loadConversationImageRevisionChain(originId: string): ConversationImageRevisionChain;
22
+ /** Append a generated version and make it the version shown by the conversation card. */
23
+ export declare function appendConversationImageRevision(originId: string, revision: ConversationImageRevision): ConversationImageRevisionChain;
24
+ /** Select an earlier or later version without altering the underlying conversation event. */
25
+ export declare function selectConversationImageRevision(originId: string, requestedIndex: number): ConversationImageRevisionChain;
@@ -0,0 +1,11 @@
1
+ import { type StudioGenerateRequest } from '../shared.js';
2
+ export interface RegeneratableImage {
3
+ provider: string;
4
+ model: string;
5
+ output?: string | undefined;
6
+ }
7
+ /** Build the workbench request that reproduces a conversation image with an edited prompt. */
8
+ export declare function conversationRegenerateRequest(image: RegeneratableImage, prompt: string, remembered?: {
9
+ ratio: string;
10
+ quality: string;
11
+ } | undefined): StudioGenerateRequest;
@@ -12,11 +12,23 @@ export interface GalleryItem {
12
12
  provider: ImageProvider;
13
13
  model: string;
14
14
  createdAt: number;
15
- aspectRatio?: string;
16
- imageSize?: string;
17
- output?: string;
15
+ aspectRatio?: string | undefined;
16
+ imageSize?: string | undefined;
17
+ output?: string | undefined;
18
18
  /** Workflow seed reported by the ComfyUI provider. Optional so records written by older versions stay readable; the object store is schemaless, so no IndexedDB version bump is needed. */
19
- seed?: number;
19
+ seed?: number | undefined;
20
+ /** Saved file path on workspace disk if savedTo was returned */
21
+ savedTo?: string | undefined;
22
+ /** Whether this item is marked as favorite */
23
+ isFavorite?: boolean | undefined;
24
+ /** Custom tags or collection markers */
25
+ tags?: string[] | undefined;
26
+ /** Workspace filesystem path where the image was generated/saved */
27
+ workspacePath?: string | undefined;
28
+ /** Workspace ID if known */
29
+ workspaceId?: string | undefined;
30
+ /** Conversation session ID where the image was generated */
31
+ sessionId?: string | undefined;
20
32
  }
21
33
  type GalleryListener = () => void;
22
34
  /**
@@ -26,6 +38,7 @@ export declare function subscribeGallery(listener: GalleryListener): () => void;
26
38
  /**
27
39
  * Save or update a gallery record by attachmentId.
28
40
  * Skipped if the item was previously deleted (tombstoned).
41
+ * Preserves existing isFavorite, tags, and original createdAt on re-renders.
29
42
  */
30
43
  export declare function saveGalleryItem(item: Omit<GalleryItem, 'createdAt'> & {
31
44
  createdAt?: number;
@@ -34,12 +47,34 @@ export declare function saveGalleryItem(item: Omit<GalleryItem, 'createdAt'> & {
34
47
  * Retrieve all gallery records sorted by createdAt descending.
35
48
  */
36
49
  export declare function getGalleryItems(): Promise<GalleryItem[]>;
50
+ /**
51
+ * Toggle favorite status of a gallery item.
52
+ * Returns the new favorite status (true if favorited, false if unfavorited).
53
+ */
54
+ export declare function toggleFavoriteGalleryItem(id: string): Promise<boolean>;
37
55
  /**
38
56
  * Delete a single gallery record by ID and record a tombstone.
39
57
  */
40
58
  export declare function deleteGalleryItem(id: string): Promise<void>;
59
+ /**
60
+ * Bulk delete multiple gallery records by IDs and record tombstones in a single transaction.
61
+ */
62
+ export declare function bulkDeleteGalleryItems(ids: string[]): Promise<void>;
41
63
  /**
42
64
  * Clear all gallery records and reset tombstones.
43
65
  */
44
66
  export declare function clearGallery(): Promise<void>;
67
+ /**
68
+ * Standardize path format across Windows and POSIX (lowercase, forward slashes, no trailing slash).
69
+ */
70
+ export declare function normalizeWorkspacePath(rawPath: string): string;
71
+ /**
72
+ * Determine if a gallery item belongs to the given workspace context.
73
+ * Compares workspaceId, sessionId membership, workspacePath, and savedTo file path prefix.
74
+ */
75
+ export declare function isItemInWorkspace(item: GalleryItem, workspace?: {
76
+ workspaceId?: string | undefined;
77
+ path?: string | undefined;
78
+ sessionIds?: readonly string[] | undefined;
79
+ } | null): boolean;
45
80
  export {};
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Native Workspace Gallery View Component for DSH `conversation.view` slot.
3
- * Fully i18n-reactive (Chinese & English).
2
+ * Native Workspace Gallery & Studio View Component for DSH `conversation.view` slot.
3
+ * Fully i18n-reactive (Chinese & English) with modular tabs, multi-dimensional filters,
4
+ * responsive image grid, and placeholder routes.
4
5
  */
5
6
  import { type FC } from 'react';
6
7
  export interface LocaleService {
@@ -9,65 +10,179 @@ export interface LocaleService {
9
10
  };
10
11
  subscribe(fn: () => void): () => void;
11
12
  }
13
+ export type TabKey = 'gallery' | 'studio' | 'favorites';
14
+ export type SortKey = 'newest' | 'oldest';
12
15
  declare const DICT: {
13
16
  readonly zh: {
14
- readonly totalCount: "共 {count} 张生成图片";
15
- readonly searchPlaceholder: "搜索 Prompt 关键词…";
16
- readonly filterAll: "全部厂商";
17
+ readonly tabGallery: "图库";
18
+ readonly tabStudio: "工作台";
19
+ readonly tabFavorites: "收藏";
20
+ readonly tabCompare: "对比";
21
+ readonly tabTasks: "任务";
22
+ readonly filterAllProviders: "全部提供商";
17
23
  readonly filterGoogle: "Google Gemini";
18
24
  readonly filterOpenAI: "OpenAI / 中转站";
19
25
  readonly filterSeedream: "字节 Seedream";
20
26
  readonly filterDashScope: "阿里 DashScope";
21
27
  readonly filterComfyUI: "本地 ComfyUI";
28
+ readonly filterAllModels: "全部模型";
29
+ readonly filterAllRatios: "全部比例";
30
+ readonly searchPlaceholder: "搜索 Prompt、标签…";
31
+ readonly filterCurrentWorkspace: "仅看此工作区";
32
+ readonly filterCurrentWorkspaceHint: "只展示属于当前工作区的生图记录与物理文件";
33
+ readonly sortNewest: "最新生成";
34
+ readonly sortOldest: "最早生成";
35
+ readonly totalCount: "共 {count} 张生成图片";
22
36
  readonly emptyTitle: "暂无生图记录";
23
37
  readonly emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。";
38
+ readonly favEmptyTitle: "暂无收藏图片";
39
+ readonly favEmptyDesc: "在图库中点击卡片右下角的 ♡ 按钮,即可将喜爱的图片收录到这里。";
24
40
  readonly noMatchTitle: "未找到匹配结果";
25
- readonly noMatchDesc: "尝试更换搜索关键词或选择其他厂商。";
41
+ readonly noMatchDesc: "尝试更换搜索关键词或调整筛选条件。";
26
42
  readonly copiedPrompt: "已复制 Prompt";
27
43
  readonly copiedImage: "已复制图片";
28
44
  readonly copyFailed: "复制失败";
45
+ readonly favoriteAdded: "已添加到收藏";
46
+ readonly favoriteRemoved: "已取消收藏";
29
47
  readonly preview: "查看大图";
30
48
  readonly download: "下载图片";
31
49
  readonly copyImg: "复制图片";
32
50
  readonly copyPpt: "复制 Prompt";
51
+ readonly regenerate: "重新生成";
52
+ readonly regenerateTitle: "重新生成图片";
53
+ readonly regenerateHint: "可按需修改 Prompt,基于当前模型和比例在画廊生成一张新图片。";
54
+ readonly confirmRegenerate: "开始生成";
55
+ readonly regenerating: "正在重新生成…";
56
+ readonly regenerateSuccess: "已生成新图片并收录到画廊";
57
+ readonly regenerateFailed: "重新生成失败";
33
58
  readonly delete: "从画廊删除";
34
59
  readonly confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)";
35
60
  readonly deleted: "已从画廊删除";
36
61
  readonly model: "模型";
37
62
  readonly prompt: "Prompt";
38
63
  readonly close: "关闭 (Esc)";
64
+ readonly prevImage: "上一张 (←)";
65
+ readonly nextImage: "下一张 (→)";
66
+ readonly manage: "批量删除";
67
+ readonly exitManage: "退出选择";
68
+ readonly selectedCount: "已选 {n} 项";
69
+ readonly selectAll: "全选";
70
+ readonly invertSelect: "反选";
71
+ readonly clearSelect: "清空";
72
+ readonly batchDelete: "批量删除";
73
+ readonly batchDeleteTitle: "确认批量删除选中的 {count} 张图片?";
74
+ readonly batchDeleteTitleSingle: "确认从画廊中删除这张图片?";
75
+ readonly batchDeleteDesc: "将从画廊历史记录中移除所选图片。";
76
+ readonly deleteWorkspaceFilesOpt: "同时清理工作区磁盘物理文件(不可恢复)";
77
+ readonly cancel: "取消";
78
+ readonly confirmBatchDelete: "确认删除 ({count})";
79
+ readonly confirmDeleteSingle: "确认删除";
80
+ readonly batchDeletedToast: "已删除 {count} 张图片";
81
+ readonly batchDeletedWithFilesToast: "已删除 {count} 张图片,并清理了 {files} 个工作区文件";
82
+ readonly deleteFailedFileLocked: "已删除 {count} 张图片,但有 {files} 个文件因被系统占用未能删除";
83
+ readonly deleteFailedDatabase: "删除失败:本地数据库操作异常";
84
+ readonly justNow: "刚刚";
85
+ readonly minutesAgo: "{n} 分钟前";
86
+ readonly hoursAgo: "{n} 小时前";
87
+ readonly daysAgo: "{n} 天前";
88
+ readonly studioTitle: "AI 图像工作台 (Studio)";
89
+ readonly studioDesc: "工作台模块正在紧锣密鼓开发中。在此你将体验大图精修、变体生成 (Variations)、参数重调并一键将生成结果无缝插回 DSH 正在进行的对话。";
90
+ readonly studioTip: "💡 提示:目前你可以在“图库”中点击任意图片,在弹窗中进行查看、复制与下载。";
91
+ readonly compareTitle: "多模型横向对比 (Compare)";
92
+ readonly compareDesc: "支持单个 Prompt 一键同时调度 Gemini、Seedream、DashScope 及本地 ComfyUI 模型并排生成,直观横评画质与细节。";
93
+ readonly tasksTitle: "异步任务队列 (Tasks)";
94
+ readonly tasksDesc: "集中管理后台批量生图、多模型并发生成与本地 ComfyUI 耗时任务。支持状态追踪、失败重试与执行耗时分析。";
95
+ readonly comingSoonBadge: "即将推出";
39
96
  };
40
97
  readonly en: {
41
- readonly totalCount: "{count} images total";
42
- readonly searchPlaceholder: "Search prompt keywords…";
43
- readonly filterAll: "All Providers";
98
+ readonly tabGallery: "Gallery";
99
+ readonly tabStudio: "Studio";
100
+ readonly tabFavorites: "Favorites";
101
+ readonly tabCompare: "Compare";
102
+ readonly tabTasks: "Tasks";
103
+ readonly filterAllProviders: "All Providers";
44
104
  readonly filterGoogle: "Google Gemini";
45
105
  readonly filterOpenAI: "OpenAI / Relay";
46
106
  readonly filterSeedream: "ByteDance Seedream";
47
107
  readonly filterDashScope: "Aliyun DashScope";
48
108
  readonly filterComfyUI: "Local ComfyUI";
109
+ readonly filterAllModels: "All Models";
110
+ readonly filterAllRatios: "All Ratios";
111
+ readonly searchPlaceholder: "Search prompt, tags…";
112
+ readonly filterCurrentWorkspace: "Current workspace only";
113
+ readonly filterCurrentWorkspaceHint: "Only show images belonging to the active workspace";
114
+ readonly sortNewest: "Newest first";
115
+ readonly sortOldest: "Oldest first";
116
+ readonly totalCount: "{count} images total";
49
117
  readonly emptyTitle: "No images generated yet";
50
118
  readonly emptyDesc: "Images generated during conversations will automatically appear here.";
119
+ readonly favEmptyTitle: "No favorite images yet";
120
+ readonly favEmptyDesc: "Click the ♡ button on any card in the gallery to collect your favorite images here.";
51
121
  readonly noMatchTitle: "No matching images";
52
- readonly noMatchDesc: "Try a different search keyword or provider filter.";
122
+ readonly noMatchDesc: "Try a different search keyword or adjust filter criteria.";
53
123
  readonly copiedPrompt: "Prompt copied";
54
124
  readonly copiedImage: "Image copied";
55
125
  readonly copyFailed: "Copy failed";
126
+ readonly favoriteAdded: "Added to favorites";
127
+ readonly favoriteRemoved: "Removed from favorites";
56
128
  readonly preview: "Full Preview";
57
129
  readonly download: "Download";
58
130
  readonly copyImg: "Copy Image";
59
131
  readonly copyPpt: "Copy Prompt";
132
+ readonly regenerate: "Regenerate";
133
+ readonly regenerateTitle: "Regenerate Image";
134
+ readonly regenerateHint: "Edit prompt if needed. A new image will be generated and added to the gallery.";
135
+ readonly confirmRegenerate: "Generate";
136
+ readonly regenerating: "Regenerating…";
137
+ readonly regenerateSuccess: "New image generated and added to gallery";
138
+ readonly regenerateFailed: "Regeneration failed";
60
139
  readonly delete: "Delete from gallery";
61
140
  readonly confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)";
62
141
  readonly deleted: "Deleted from gallery";
63
142
  readonly model: "Model";
64
143
  readonly prompt: "Prompt";
65
144
  readonly close: "Close (Esc)";
145
+ readonly prevImage: "Previous (←)";
146
+ readonly nextImage: "Next (→)";
147
+ readonly manage: "Batch Delete";
148
+ readonly exitManage: "Done";
149
+ readonly selectedCount: "{n} selected";
150
+ readonly selectAll: "Select All";
151
+ readonly invertSelect: "Invert";
152
+ readonly clearSelect: "Clear";
153
+ readonly batchDelete: "Batch Delete";
154
+ readonly batchDeleteTitle: "Delete {count} selected images?";
155
+ readonly batchDeleteTitleSingle: "Delete this image from gallery?";
156
+ readonly batchDeleteDesc: "These images will be removed from your gallery history.";
157
+ readonly deleteWorkspaceFilesOpt: "Also delete files from workspace disk (cannot be undone)";
158
+ readonly cancel: "Cancel";
159
+ readonly confirmBatchDelete: "Delete ({count})";
160
+ readonly confirmDeleteSingle: "Delete";
161
+ readonly batchDeletedToast: "Deleted {count} images";
162
+ readonly batchDeletedWithFilesToast: "Deleted {count} images and cleaned {files} workspace files";
163
+ readonly deleteFailedFileLocked: "Deleted {count} images, but {files} files could not be deleted (busy/locked)";
164
+ readonly deleteFailedDatabase: "Failed to delete: local database error";
165
+ readonly justNow: "Just now";
166
+ readonly minutesAgo: "{n}m ago";
167
+ readonly hoursAgo: "{n}h ago";
168
+ readonly daysAgo: "{n}d ago";
169
+ readonly studioTitle: "AI Image Studio";
170
+ readonly studioDesc: "Studio workbench is under active development. Fine-tune prompts, generate variations (2x/4x), and inject images directly into DSH chat.";
171
+ readonly studioTip: "💡 Tip: You can currently click any image in the Gallery to preview, copy, or download it.";
172
+ readonly compareTitle: "Model Comparison (Compare)";
173
+ readonly compareDesc: "Side-by-side multi-model benchmarking coming soon. Test Gemini, Seedream, DashScope, and ComfyUI with a single prompt.";
174
+ readonly tasksTitle: "Task Queue (Tasks)";
175
+ readonly tasksDesc: "Centralized view for batch generation, asynchronous ComfyUI runs, live progress tracking, and retry controls.";
176
+ readonly comingSoonBadge: "Coming Soon";
66
177
  };
67
178
  };
68
179
  export type DictKey = keyof typeof DICT.zh;
69
- export declare const GalleryViewTab: FC<{
180
+ export interface GalleryViewTabProps {
70
181
  locale?: LocaleService;
71
- }>;
72
- export declare function copyImageBlob(blob: Blob): Promise<boolean>;
73
- export {};
182
+ sessionId?: string;
183
+ useSession?: (selector: (state: any) => any) => any;
184
+ useSessions?: (selector: (state: any) => any) => any;
185
+ useWorkspaces?: (selector: (state: any) => any) => any;
186
+ }
187
+ export declare const GalleryViewTab: FC<GalleryViewTabProps>;
188
+ export { copyImageBlob } from './browser-image-utils.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Bounded in-memory attachment image cache for client views.
3
+ *
4
+ * Enforces dual constraints (max items + max bytes) with LRU eviction.
5
+ * The background fetch lifecycle is decoupled from individual consumer signals,
6
+ * preventing a canceled thumbnail from aborting the shared blob request.
7
+ */
8
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
9
+ export declare const MAX_CACHE_COUNT = 30;
10
+ export declare const MAX_CACHE_BYTES: number;
11
+ /**
12
+ * Fetch or reuse an image Blob by attachment ID.
13
+ * Refreshes LRU recency on hit and enforces count/byte limits on arrival.
14
+ */
15
+ export declare function fetchAttachmentBlob(attachment: ImageAttachmentRef): Promise<Blob>;
16
+ /** Check if an attachment Blob is already cached in memory. */
17
+ export declare function getCachedAttachmentBlob(attachmentId: string | ImageAttachmentRef['attachmentId']): Promise<Blob> | undefined;
18
+ /** Evict one item from memory cache (e.g. when deleted from gallery). */
19
+ export declare function evictAttachmentCache(attachmentId: string | ImageAttachmentRef['attachmentId']): void;
20
+ /** Clear all in-memory image caches. */
21
+ export declare function clearAttachmentCache(): void;
22
+ /** Get current cache size metrics (useful for assertions and health monitoring). */
23
+ export declare function getCacheMetrics(): {
24
+ count: number;
25
+ bytes: number;
26
+ };
@@ -0,0 +1 @@
1
+ export declare const STUDIO_STYLE = "\n.dsh-ig-gallery-page-body.is-workbench{flex:1 1 0;height:100%;min-height:0;padding:0;overflow:hidden;display:flex;flex-direction:column}\n.dsh-ig-workbench{--ig-blue:#2f64f5;--ig-line:color-mix(in srgb,currentColor 11%,transparent);--ig-soft:color-mix(in srgb,currentColor 4%,transparent);position:relative;display:flex;flex-direction:column;min-height:0;height:100%;overflow:hidden;padding:10px 12px 12px;color:var(--foreground,#182033);box-sizing:border-box;background:radial-gradient(circle at 60% -20%,rgba(63,104,246,.07),transparent 30%)}\n.dsh-ig-workbench *{box-sizing:border-box}.dsh-ig-workbench button,.dsh-ig-workbench select,.dsh-ig-workbench textarea{font:inherit;color:inherit}\n.dsh-ig-workbench-grid{display:grid;grid-template-columns:minmax(190px,238px) minmax(0,1fr) minmax(286px,340px);grid-template-rows:minmax(0,1fr);min-height:0;height:0;flex:1 1 0;border:1px solid var(--ig-line);border-radius:12px;overflow:hidden;background:var(--background,#fff);box-shadow:0 12px 32px rgba(30,42,76,.05);transition:grid-template-columns .2s ease}\n.dsh-ig-workbench-grid.is-sidebar-collapsed{grid-template-columns:minmax(0,1fr) minmax(286px,340px)}\n.dsh-ig-recent-panel,.dsh-ig-generate-panel{min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--background,#fff) 96%,#f5f7fb)}.dsh-ig-recent-panel{display:flex;flex-direction:column;border-right:1px solid var(--ig-line)}\n.dsh-ig-panel-title{height:48px;flex:0 0 48px;padding:0 12px 0 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--ig-line);font-size:13px;font-weight:650}\n.dsh-ig-count-badge{font-size:11px;min-width:22px;text-align:center;padding:2px 6px;border-radius:999px;background:var(--ig-soft);color:#7b8498;font-weight:500}\n.dsh-ig-collapse-btn{width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:6px;background:transparent;color:#717b90;cursor:pointer;transition:all .15s}\n.dsh-ig-collapse-btn:hover{border-color:var(--ig-line);background:var(--ig-soft);color:var(--foreground,#182033)}\n.dsh-ig-recent-scroll{flex:1 1 0;height:0;padding:8px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-recent-empty{min-height:180px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;color:#929bad;text-align:center;font-size:12px}\n.dsh-ig-recent-item{display:grid;grid-template-columns:70px minmax(0,1fr);gap:10px;width:100%;padding:8px;border:1px solid transparent;border-radius:9px;background:transparent;text-align:left;cursor:pointer;transition:background .16s,border-color .16s,transform .16s}.dsh-ig-recent-item:hover{background:var(--ig-soft);transform:translateY(-1px)}.dsh-ig-recent-item.is-active{border-color:#89a7ff;background:rgba(47,100,245,.075);box-shadow:inset 3px 0 0 var(--ig-blue)}.dsh-ig-recent-thumb{width:70px;height:70px;border-radius:7px;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#eef1f7;color:#929bad}.dsh-ig-recent-thumb img{width:100%;height:100%;object-fit:cover}.dsh-ig-recent-item>div:last-child{min-width:0;padding:1px 0}.dsh-ig-recent-item strong{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:12.5px;line-height:19px;font-weight:630}.dsh-ig-recent-item span,.dsh-ig-recent-item small{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#828b9e;font-size:10.5px;line-height:16px}\n.dsh-ig-load-more{width:100%;height:32px;margin:8px 0;display:flex;align-items:center;justify-content:center;border:1px dashed var(--ig-line);border-radius:7px;background:transparent;font-size:11px;color:#6b758b;cursor:pointer;transition:background .15s,color .15s}.dsh-ig-load-more:hover{background:var(--ig-soft);color:var(--ig-blue);border-color:#a2b9f6}\n.dsh-ig-mini-btn{height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:3px;border:1px solid var(--ig-line);border-radius:5px;background:var(--background,#fff);font-size:10.5px;font-weight:500;color:var(--foreground,#242f44);cursor:pointer;transition:all .15s}.dsh-ig-mini-btn:hover{border-color:#9bb4f5;color:var(--ig-blue);background:rgba(47,100,245,.06)}\n.dsh-ig-canvas-column{min-width:0;min-height:0;height:100%;overflow:hidden;display:flex;flex-direction:column;background:color-mix(in srgb,var(--background,#fff) 97%,#e8edf7)}\n.dsh-ig-canvas-toolbar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 14px;border-bottom:1px solid var(--ig-line)}\n.dsh-ig-canvas-toolbar-left,.dsh-ig-canvas-toolbar-right{display:flex;align-items:center;gap:6px}\n.dsh-ig-canvas-toolbar button{height:30px;min-width:30px;padding:0 9px;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);color:var(--foreground,#242f44);font-size:11.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s,color .15s}\n.dsh-ig-canvas-toolbar button:hover{border-color:#9bb4f5;background:rgba(47,100,245,.05);color:var(--ig-blue)}\n.dsh-ig-canvas-toolbar button.is-active{border-color:#8ba8f8;background:rgba(47,100,245,.08);color:var(--ig-blue);font-weight:550}\n.dsh-ig-zoom-display{font-size:11px;min-width:38px;text-align:center;color:#64748b;user-select:none;font-variant-numeric:tabular-nums}\n.dsh-ig-status-badge-wrap{position:relative;display:inline-flex;align-items:center}\n.dsh-ig-status-badge{height:30px;display:inline-flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);cursor:pointer;user-select:none;transition:border-color .15s,background .15s,box-shadow .15s}\n.dsh-ig-status-badge:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}\n.dsh-ig-status-dot{width:7px;height:7px;min-width:7px;max-width:7px;border-radius:50%;flex-shrink:0;box-sizing:border-box}\n.dsh-ig-status-dot.is-ready{background:#10b981;box-shadow:0 0 0 2px rgba(16,185,129,.2)}\n.dsh-ig-status-dot.is-muted{background:#ef4444;box-shadow:0 0 0 2px rgba(239,68,68,.2)}\n.dsh-ig-status-text{font-size:11.5px;font-weight:500;color:var(--foreground,#242f44);line-height:1}\n.dsh-ig-status-arrow{color:#94a3b8;transition:transform .15s,color .15s}\n.dsh-ig-status-badge-wrap:hover .dsh-ig-status-arrow{transform:rotate(180deg);color:var(--ig-blue)}\n.dsh-ig-status-popover{position:absolute;left:0;top:calc(100% + 6px);width:260px;background:var(--background,#fff);border:1px solid var(--ig-line);border-radius:9px;box-shadow:0 12px 36px rgba(20,28,45,.14);padding:10px 12px;z-index:40;opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .15s cubic-bezier(.16,1,.3,1),transform .15s cubic-bezier(.16,1,.3,1)}\n.dsh-ig-status-badge-wrap:hover .dsh-ig-status-popover{opacity:1;pointer-events:auto;transform:translateY(0)}\n.dsh-ig-status-popover-title{display:flex;align-items:center;justify-content:space-between;font-size:10px;font-weight:650;color:#8590a6;margin-bottom:8px;letter-spacing:.02em;text-transform:uppercase}\n.dsh-ig-status-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}\n.dsh-ig-status-list li{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;border-radius:6px;transition:background .12s}\n.dsh-ig-status-list li.is-configured{background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.12)}\n.dsh-ig-status-list li.is-missing{background:rgba(239,68,68,.04);border:1px solid rgba(239,68,68,.12);opacity:.75}\n.dsh-ig-status-item-name{display:flex;flex-direction:column;min-width:0}\n.dsh-ig-status-item-name strong{font-size:11.5px;font-weight:600;color:var(--foreground,#182033);line-height:16px}\n.dsh-ig-status-item-name small{font-size:9.5px;color:#7c869a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:145px;line-height:14px}\n.dsh-ig-status-item-tag{font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;flex-shrink:0}\n.is-configured .dsh-ig-status-item-tag{color:#059669;background:rgba(16,185,129,.14)}\n.is-missing .dsh-ig-status-item-tag{color:#dc2626;background:rgba(239,68,68,.14)}\n.dsh-ig-canvas{position:relative;flex:1 1 0;height:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:24px;touch-action:none;user-select:none;cursor:grab;background-image:linear-gradient(45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(-45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(114,128,156,.035) 75%),linear-gradient(-45deg,transparent 75%,rgba(114,128,156,.035) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0}\n.dsh-ig-canvas.is-dragging{cursor:grabbing!important}\n.dsh-ig-canvas img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;border-radius:10px;box-shadow:0 14px 45px rgba(25,35,63,.18);user-select:none;-webkit-user-drag:none;pointer-events:auto}\n.dsh-ig-canvas-empty,.dsh-ig-generating-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;color:#929bad;font-size:12px;padding:26px;cursor:default}\n.dsh-ig-generating-state strong{font-size:15px;color:var(--foreground,#182033)}.dsh-ig-generating-state span{font-size:11px}.dsh-ig-generation-orbit{width:66px;height:66px;border-radius:50%;display:grid;place-items:center;color:var(--ig-blue);background:rgba(47,100,245,.09);box-shadow:0 0 0 10px rgba(47,100,245,.035);animation:dshIgPulse 1.8s ease-in-out infinite}\n.dsh-ig-result-strip{flex:0 0 auto;min-height:52px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-strip>span{font-size:12px;font-weight:650}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{display:flex;gap:7px}.dsh-ig-result-strip button,.dsh-ig-result-actions button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:11px;cursor:pointer}.dsh-ig-result-strip button:hover,.dsh-ig-result-actions button:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-result-actions{flex:0 0 auto;min-height:54px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-actions p{margin:0;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11.5px;line-height:18px;color:#596277}.dsh-ig-result-meta{flex:0 0 auto;min-height:32px;padding:6px 14px;display:flex;gap:13px;align-items:center;flex-wrap:wrap;border-top:1px solid var(--ig-line);font-size:10px;color:#8790a3}.dsh-ig-result-meta span+span:before{content:'\u00B7';margin-right:13px}\n.dsh-ig-generate-panel{height:100%;display:flex;flex-direction:column;border-left:1px solid var(--ig-line);overflow:hidden}.dsh-ig-panel-tabs{height:48px;flex:0 0 48px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--ig-line);padding:0 14px}.dsh-ig-panel-tabs button{position:relative;border:0;background:transparent;font-size:12px;color:#7c8598;cursor:pointer}.dsh-ig-panel-tabs button.is-active{color:var(--ig-blue);font-weight:650}.dsh-ig-panel-tabs button.is-active:after{content:'';position:absolute;left:20%;right:20%;bottom:-1px;height:2px;background:var(--ig-blue);border-radius:2px 2px 0 0}.dsh-ig-generator-form{flex:1 1 0;height:0;padding:15px;display:flex;flex-direction:column;gap:15px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-mode-switch{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-ig-mode-switch button{height:39px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;cursor:pointer}.dsh-ig-mode-switch button.is-active{border-color:#789bff;color:var(--ig-blue);background:rgba(47,100,245,.065);box-shadow:inset 0 0 0 1px rgba(47,100,245,.1)}\n.dsh-ig-field{position:relative;display:flex;flex-direction:column;gap:7px}.dsh-ig-field>label,.dsh-ig-field-label label{font-size:11.5px;font-weight:630}.dsh-ig-field>label span{font-weight:400;color:#929bad}.dsh-ig-field-label{display:flex;align-items:center;justify-content:space-between}.dsh-ig-field-label b{color:#e14d4d}.dsh-ig-field-label button{border:0;background:transparent;color:#798397;font-size:10.5px;cursor:pointer}.dsh-ig-field textarea{width:100%;height:112px;padding:11px 12px 22px;resize:vertical;border:1px solid var(--ig-line);border-radius:9px;background:var(--background,#fff);font-size:12px;line-height:19px;outline:none}.dsh-ig-field textarea:focus{border-color:var(--ig-blue);box-shadow:0 0 0 3px rgba(47,100,245,.09)}.dsh-ig-field>small{position:absolute;right:9px;bottom:7px;color:#9aa2b2;font-size:9.5px}.dsh-ig-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.dsh-ig-field-select{display:flex;flex-direction:column;gap:6px;min-width:0}.dsh-ig-field-select>span{font-size:11px;font-weight:620}.dsh-ig-field-select select{width:100%;height:36px;padding:0 26px 0 9px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);font-size:11px;outline:none;text-overflow:ellipsis}.dsh-ig-field-select select:focus{border-color:var(--ig-blue)}\n.dsh-ig-upload{height:92px;border:1px dashed #bcc6d9;border-radius:9px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:#7d879b;cursor:pointer}.dsh-ig-upload strong{font-size:11px;font-weight:570}.dsh-ig-upload small{font-size:9.5px}.dsh-ig-upload:hover,.dsh-ig-upload.is-dragging{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}\n.dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}\n.dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}\n.dsh-ig-reference-card img{width:100%;height:100%;object-fit:cover}\n.dsh-ig-reference-card button{position:absolute;right:4px;top:4px;width:18px;height:18px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(20,28,45,.75);color:#fff;cursor:pointer;padding:0;transition:background .15s}\n.dsh-ig-reference-card button:hover{background:#ef4444}\n.dsh-ig-reference-add{aspect-ratio:1;border:1px dashed #bcc6d9;border-radius:8px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#7d879b;cursor:pointer;font-size:10px;transition:border-color .15s,color .15s,background .15s}\n.dsh-ig-reference-add:hover{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}\n.dsh-ig-reference-preview{position:relative;height:108px;border-radius:9px;overflow:hidden;background:#eef1f7}.dsh-ig-reference-preview img{width:100%;height:100%;object-fit:contain}.dsh-ig-reference-preview button{position:absolute;right:7px;top:7px;width:25px;height:25px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(20,28,45,.72);color:#fff;cursor:pointer}\n.dsh-ig-inline-note,.dsh-ig-form-error{padding:9px 10px;border-radius:7px;font-size:10.5px;line-height:16px}.dsh-ig-inline-note{background:#fff8e7;color:#8b671c;border:1px solid #f1dfae}.dsh-ig-form-error{background:#fff1f1;color:#b63737;border:1px solid #f2caca}.dsh-ig-form-loading{height:70px;display:grid;place-items:center;color:var(--ig-blue)}\n.dsh-ig-config-error{padding:14px;display:flex;flex-direction:column;align-items:center;gap:10px;background:#fff1f1;border:1px solid #f2caca;border-radius:8px;color:#b63737;font-size:11.5px;text-align:center}.dsh-ig-config-error p{margin:0;line-height:17px}.dsh-ig-config-error button{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 12px;border:1px solid #e0a8a8;border-radius:6px;background:#fff;color:#b63737;font-size:11.5px;font-weight:600;cursor:pointer}.dsh-ig-config-error button:hover{background:#ffebeb}\n.dsh-ig-generate-button{height:42px;flex:0 0 auto;border:0;border-radius:8px;background:linear-gradient(135deg,#386cf6,#2758e7);color:#fff!important;display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;font-weight:650;cursor:pointer;box-shadow:0 8px 18px rgba(47,100,245,.22);transition:transform .16s,box-shadow .16s}.dsh-ig-generate-button:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 10px 24px rgba(47,100,245,.28)}.dsh-ig-generate-button:disabled{opacity:.55;cursor:not-allowed;box-shadow:none}\n.dsh-ig-generate-button.is-cancel{background:linear-gradient(135deg,#ef4444,#dc2626);box-shadow:0 8px 18px rgba(239,68,68,.25)}.dsh-ig-generate-button.is-cancel:hover{background:linear-gradient(135deg,#dc2626,#b91c1c);box-shadow:0 10px 24px rgba(239,68,68,.35)}\n.dsh-ig-details{padding:16px;margin:0;overflow:auto}.dsh-ig-details>div{padding:12px 0;border-bottom:1px solid var(--ig-line)}.dsh-ig-details dt{font-size:10.5px;color:#8992a4;margin-bottom:5px}.dsh-ig-details dd{margin:0;font-size:11.5px;line-height:18px;word-break:break-word}.dsh-ig-details-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;height:220px;padding:20px;color:#929bad;font-size:11px;text-align:center}\n.dsh-ig-workbench-modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.45);backdrop-filter:blur(3px);z-index:999;display:grid;place-items:center;padding:16px}\n.dsh-ig-workbench-modal-box{width:100%;max-width:380px;background:var(--background,#fff);border-radius:12px;padding:20px;box-shadow:0 20px 48px rgba(0,0,0,.22);display:flex;flex-direction:column;gap:14px}\n.dsh-ig-workbench-modal-header{display:flex;align-items:center;gap:10px}\n.dsh-ig-workbench-modal-header strong{font-size:14px;font-weight:650;color:#b91c1c}\n.dsh-ig-workbench-modal-desc{font-size:12px;line-height:18px;color:#555f77;margin:0}\n.dsh-ig-workbench-modal-option{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#4b5563;cursor:pointer}\n.dsh-ig-workbench-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}\n.dsh-ig-workbench-modal-actions button{height:32px;padding:0 14px;border-radius:7px;font-size:11.5px;font-weight:600;cursor:pointer}\n.dsh-ig-workbench-modal-cancel{border:1px solid var(--ig-line);background:var(--background,#fff);color:#6b7280}\n.dsh-ig-workbench-modal-cancel:hover{background:#f3f4f6}\n.dsh-ig-workbench-modal-danger{border:0;background:#dc2626;color:#fff}\n.dsh-ig-workbench-modal-danger:hover{background:#b91c1c}\n@keyframes dshIgPulse{50%{transform:scale(1.06);box-shadow:0 0 0 16px rgba(47,100,245,.025)}}\n.dsh-ig-workbench-toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;padding:8px 18px;border-radius:10px;font-size:12.5px;font-weight:500;box-shadow:0 12px 32px rgba(0,0,0,0.3);z-index:99999;animation:dshIgFade .15s ease-out;pointer-events:none}\n@keyframes dshIgFade{from{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%,0)}}\n@media(max-width:1120px){.dsh-ig-workbench-grid{grid-template-columns:190px minmax(360px,1fr) 300px}.dsh-ig-result-actions button span{display:none}}\n@media(max-width:900px){.dsh-ig-gallery-page-body.is-workbench{height:auto;overflow:auto}.dsh-ig-workbench{height:auto;min-height:100%;overflow:visible;padding:12px}.dsh-ig-workbench-heading{height:auto;padding-bottom:12px;align-items:center}.dsh-ig-provider-quick>span{display:none}.dsh-ig-workbench-grid{height:auto;display:flex;flex:none;flex-direction:column;overflow:visible}.dsh-ig-recent-panel{height:auto;overflow:hidden;border-right:0;border-bottom:1px solid var(--ig-line)}.dsh-ig-recent-scroll{height:auto;display:flex;overflow-x:auto;overflow-y:hidden;padding:8px}.dsh-ig-recent-item{min-width:220px}.dsh-ig-canvas-column{height:auto;min-height:560px}.dsh-ig-generate-panel{height:auto;border-left:0;border-top:1px solid var(--ig-line);min-height:560px}.dsh-ig-generator-form{height:auto;overflow:visible}.dsh-ig-canvas{height:auto;min-height:360px}}\n@media(max-width:560px){.dsh-ig-workbench-heading>div{display:block}.dsh-ig-workbench-heading h1{font-size:15px}.dsh-ig-provider-quick select{min-width:128px;max-width:150px}.dsh-ig-canvas{padding:12px;min-height:300px}.dsh-ig-result-strip,.dsh-ig-result-actions{align-items:flex-start;flex-direction:column}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{width:100%}.dsh-ig-result-strip button,.dsh-ig-result-actions button{flex:1;justify-content:center}.dsh-ig-result-actions button span{display:inline}.dsh-ig-field-grid{grid-template-columns:1fr}.dsh-ig-result-meta span+span:before{display:none}}\n@media(prefers-reduced-motion:reduce){.dsh-ig-workbench *{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}\n.dsh-ig-workspace-filter-label{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line,rgba(0,0,0,.1));border-radius:7px;background:var(--background,#fff);font-size:11.5px;color:var(--foreground,#242f44);cursor:pointer;user-select:none;transition:border-color .15s,background .15s}\n.dsh-ig-workspace-filter-label:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}\n.dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}\n.dsh-ig-workspace-filter-text{font-weight:500;white-space:nowrap}\n.dsh-ig-workspace-filter-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,currentColor 7%,transparent);color:#64748b;font-variant-numeric:tabular-nums}\n";
@@ -0,0 +1,30 @@
1
+ import { type FC } from 'react';
2
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
3
+ export interface LocaleService {
4
+ subscribe(cb: () => void): () => void;
5
+ getSnapshot(): {
6
+ active?: string;
7
+ };
8
+ }
9
+ export interface StudioReferenceItem {
10
+ id: string;
11
+ attachment?: ImageAttachmentRef;
12
+ file?: File;
13
+ previewUrl: string;
14
+ }
15
+ export interface StudioWorkspaceProps {
16
+ workspaceId?: string | undefined;
17
+ path?: string | undefined;
18
+ title?: string | undefined;
19
+ sessionIds?: readonly string[] | undefined;
20
+ }
21
+ export declare const StudioView: FC<{
22
+ locale?: LocaleService | undefined;
23
+ workspace?: StudioWorkspaceProps | null | undefined;
24
+ }>;
25
+ export declare function useAttachmentImage(attachment: ImageAttachmentRef | undefined, shouldLoad?: boolean): {
26
+ url: string | null;
27
+ blob: Blob | null;
28
+ loading: boolean;
29
+ };
30
+ export declare function fileToBase64(file: File): Promise<string>;
@@ -1,12 +1,18 @@
1
1
  /** Same-origin HTTP bridge from the Web result card to the Attachment service. */
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import type { ImageAttachmentRef, StoredImageAttachment } from '@deepseek-ai/dsh-attachment';
4
- export { IMAGE_ROUTE } from './shared.js';
4
+ export { IMAGE_ROUTE, DELETE_ROUTE } from './shared.js';
5
5
  /** Dependencies required by the image route. */
6
6
  export interface ImageRouteDeps {
7
7
  readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>;
8
8
  }
9
+ /** Dependencies required by the delete route. */
10
+ export interface DeleteRouteDeps {
11
+ deleteWorkspaceImage(filePath: string): Promise<boolean>;
12
+ }
9
13
  /** Serve one verified durable image reference to a same-origin browser request. */
10
14
  export declare function serveImage(req: IncomingMessage, res: ServerResponse, deps: ImageRouteDeps): Promise<void>;
15
+ /** Safely delete one or more generated image files from the workspace disk. */
16
+ export declare function serveDelete(req: IncomingMessage, res: ServerResponse, deps: DeleteRouteDeps): Promise<void>;
11
17
  /** Validate the persisted reference carried by a tool presentation. */
12
18
  export declare function imageAttachmentFromMeta(meta: unknown): ImageAttachmentRef | undefined;
@@ -2,7 +2,8 @@
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import { Config } from './config.js';
4
4
  export { Config } from './config.js';
5
- export { IMAGE_ROUTE, imageAttachmentFromMeta } from './image-route.js';
5
+ export { IMAGE_ROUTE, DELETE_ROUTE, imageAttachmentFromMeta } from './image-route.js';
6
+ export { STUDIO_ROUTE } from './shared.js';
6
7
  export declare const name = "dsh-image-gen";
7
8
  export declare const inject: string[];
8
9
  export declare function apply(ctx: Context, config?: Config): void;
@@ -1,11 +1,84 @@
1
1
  /** Values shared by the Host and browser Bundle faces. */
2
+ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
2
3
  /** Browser route used by the generated-image card. */
3
4
  export declare const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
5
+ /** Browser route used for deleting generated images and workspace files. */
6
+ export declare const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
7
+ /** Same-origin route used by the browser image workbench. */
8
+ export declare const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
4
9
  /** Namespace persisted through DSH Settings. */
5
10
  export declare const IMAGE_GENERATION_NAMESPACE = "image-generation";
6
11
  /** Supported providers. */
7
12
  export declare const IMAGE_PROVIDERS: readonly ["google", "openai", "seedream", "dashscope", "comfyui"];
8
13
  export type ImageProvider = typeof IMAGE_PROVIDERS[number];
14
+ /** Providers supported by the first browser workbench release. */
15
+ export declare const CLOUD_IMAGE_PROVIDERS: readonly ["google", "openai", "seedream", "dashscope"];
16
+ export type CloudImageProvider = typeof CLOUD_IMAGE_PROVIDERS[number];
17
+ /** One selectable output option exposed by a provider profile. */
18
+ export interface StudioOption {
19
+ value: string;
20
+ label: string;
21
+ }
22
+ /** Browser-safe provider description. Credentials and endpoints never cross this boundary. */
23
+ export interface StudioProviderProfile {
24
+ provider: CloudImageProvider;
25
+ label: string;
26
+ model: string;
27
+ configured: boolean;
28
+ supportsEditing: boolean;
29
+ ratioOptions: StudioOption[];
30
+ qualityOptions: StudioOption[];
31
+ defaultRatio: string;
32
+ defaultQuality: string;
33
+ }
34
+ /** Lightweight workspace description exposed to the client for scoping. */
35
+ export interface StudioWorkspaceInfo {
36
+ workspaceId: string;
37
+ path: string;
38
+ title: string;
39
+ sessionIds: string[];
40
+ }
41
+ /** Read model and capability state for the workbench without exposing secrets. */
42
+ export interface StudioConfigResponse {
43
+ providers: StudioProviderProfile[];
44
+ activeProvider: CloudImageProvider;
45
+ workspaceRoot?: string | undefined;
46
+ workspaces?: StudioWorkspaceInfo[] | undefined;
47
+ }
48
+ /** A browser-uploaded reference image used for one editing request. */
49
+ export interface StudioEncodedReference {
50
+ mediaType: ImageMediaType;
51
+ data: string;
52
+ name?: string;
53
+ }
54
+ /** A durable image reference selected from the existing gallery. */
55
+ export interface StudioAttachmentReference {
56
+ attachment: ImageAttachmentRef;
57
+ }
58
+ export type StudioReference = StudioEncodedReference | StudioAttachmentReference;
59
+ /** One browser workbench generation or editing request. */
60
+ export interface StudioGenerateRequest {
61
+ mode: 'generate' | 'edit';
62
+ provider: CloudImageProvider;
63
+ model: string;
64
+ prompt: string;
65
+ ratio: string;
66
+ quality: string;
67
+ reference?: StudioReference;
68
+ references?: StudioReference[];
69
+ workspaceRoot?: string | undefined;
70
+ }
71
+ /** One completed workbench request. */
72
+ export interface StudioGenerateResponse {
73
+ attachment: ImageAttachmentRef;
74
+ provider: CloudImageProvider;
75
+ model: string;
76
+ prompt: string;
77
+ output: string;
78
+ createdAt: number;
79
+ elapsedMs: number;
80
+ savedTo?: string | undefined;
81
+ }
9
82
  /** Default endpoints and base URLs. */
10
83
  export declare const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
11
84
  export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -0,0 +1,12 @@
1
+ /** Same-origin HTTP bridge used by the browser image workbench. */
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { type StudioConfigResponse, type StudioGenerateRequest, type StudioGenerateResponse } from './shared.js';
4
+ export interface StudioRouteDeps {
5
+ describe(): Promise<StudioConfigResponse>;
6
+ generate(input: StudioGenerateRequest, signal: AbortSignal): Promise<StudioGenerateResponse>;
7
+ maxBodyBytes: number;
8
+ }
9
+ /** Serve workbench capabilities and generation requests without exposing provider credentials. */
10
+ export declare function serveStudio(req: IncomingMessage, res: ServerResponse, deps: StudioRouteDeps): Promise<void>;
11
+ /** Strictly validate the small untrusted workbench wire contract. */
12
+ export declare function parseStudioGenerateRequest(value: unknown): StudioGenerateRequest;
@@ -0,0 +1,8 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type Config } from './config.js';
3
+ import { type CloudImageProvider, type StudioConfigResponse, type StudioGenerateRequest, type StudioGenerateResponse, type StudioProviderProfile } from './shared.js';
4
+ /** Return only browser-safe capability data. */
5
+ export declare function describeStudio(ctx: Context, config: Config): Promise<StudioConfigResponse>;
6
+ /** Execute one validated browser workbench request using the existing provider adapters. */
7
+ export declare function generateFromStudio(ctx: Context, config: Config, input: StudioGenerateRequest, signal: AbortSignal, fallbackWorkspaceRoot?: string | undefined): Promise<StudioGenerateResponse>;
8
+ export declare function studioProfile(config: Config, provider: CloudImageProvider, configured: boolean): StudioProviderProfile;
@@ -1,3 +1,4 @@
1
+ import type { StudioWorkspaceInfo } from './shared.js';
1
2
  import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
2
3
  /**
3
4
  * Build the deterministic file name for a generated image:
@@ -50,3 +51,26 @@ export declare function saveImageToWorkspace(options: {
50
51
  data: Uint8Array;
51
52
  signal?: AbortSignal;
52
53
  }): Promise<string>;
54
+ /** Discover all DSH workspace paths recorded in local application storage. */
55
+ export declare function getDshWorkspaceRoots(): Promise<string[]>;
56
+ /** Discover detailed workspace records from DSH workspace.json storage. */
57
+ export declare function getDshWorkspacesFull(): Promise<StudioWorkspaceInfo[]>;
58
+ /**
59
+ * Safely delete a generated image file from workspace disk by its path.
60
+ * Enforces strict safety gates:
61
+ * 1. Base name must match generated image pattern (`image-<hex>.<ext>`) to avoid touching user files.
62
+ * 2. Path must be a regular file and NOT a symbolic link.
63
+ * 3. Real canonical path's basename must also match the pattern.
64
+ * 4. Real canonical path must reside within an allowed workspace root (or process.cwd()).
65
+ */
66
+ export declare function deleteImageFromWorkspace(filePath: string, allowedWorkspaceRoots?: Iterable<string>): Promise<boolean>;
67
+ /**
68
+ * Safely find and delete a generated image file by its attachment ID within a single workspace root.
69
+ * Strictly checks sha256:64hex format and avoids scanning unrelated workspaces.
70
+ * Only used as fallback for historical images where savedTo was not persisted.
71
+ */
72
+ export declare function deleteImageByAttachmentIdFromWorkspace(attachmentId: string, options?: {
73
+ workspaceRoot?: string | undefined;
74
+ folder?: string | undefined;
75
+ allowedWorkspaceRoots?: Iterable<string> | undefined;
76
+ }): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-image-gen",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Bring ChatGPT-like image generation to DeepSeek Harness — Gemini, OpenAI, Seedream, DashScope, local ComfyUI & more.",
5
5
  "type": "module",
6
6
  "author": "shanliuling",
@@ -54,13 +54,6 @@
54
54
  "platform": "web"
55
55
  }
56
56
  },
57
- "scripts": {
58
- "typecheck": "tsc --noEmit",
59
- "build": "tsc && tsdown",
60
- "prepare": "pnpm run build",
61
- "test": "vitest run",
62
- "pack:check": "pnpm pack --dry-run"
63
- },
64
57
  "engines": {
65
58
  "node": "^22.19.0 || >=24.0.0"
66
59
  },
@@ -130,5 +123,13 @@
130
123
  "unrun": "^0.3.1",
131
124
  "vitest": "^4.1.8"
132
125
  },
133
- "packageManager": "pnpm@11.7.0"
134
- }
126
+ "dependencies": {
127
+ "lucide-react": "^1.38.0"
128
+ },
129
+ "scripts": {
130
+ "typecheck": "tsc --noEmit",
131
+ "build": "tsc && tsdown",
132
+ "test": "vitest run",
133
+ "pack:check": "pnpm pack --dry-run"
134
+ }
135
+ }