@chengxianglibra/dsh-data-analysis 0.2.0 → 0.2.2

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.
Files changed (48) hide show
  1. package/README.en.md +138 -0
  2. package/README.md +55 -82
  3. package/lib/client/credentials/install.js +3 -3
  4. package/lib/client/i18n/copy.js +4 -1
  5. package/lib/client/i18n/host.js +1 -0
  6. package/lib/client/i18n/settings.js +30 -0
  7. package/lib/client/input-context.js +1 -1
  8. package/lib/client/python-tool/card.js +2 -2
  9. package/lib/client/python-tool/model.js +1 -1
  10. package/lib/client/right-tabs/install.js +10 -7
  11. package/lib/client/settings/card.js +90 -0
  12. package/lib/client/settings/install.js +14 -0
  13. package/lib/client.js +104 -70
  14. package/lib/datasource/changes-contract.js +6 -2
  15. package/lib/datasource/configure.js +1 -1
  16. package/lib/datasource/python-options.js +1 -7
  17. package/lib/datasource/python.js +12 -7
  18. package/lib/datasource/test.js +1 -1
  19. package/lib/disclosure/execution-guidance.js +5 -8
  20. package/lib/plugin-agents.js +2 -2
  21. package/lib/plugin.js +14 -4
  22. package/lib/presentation/assets/portable.js +1 -1
  23. package/lib/presentation/assets/static.js +47 -47
  24. package/lib/presentation/execution-policy.js +5 -0
  25. package/lib/presentation/projection/index.js +27 -10
  26. package/lib/presentation/tool.js +2 -1
  27. package/lib/settings.js +17 -0
  28. package/lib/types/client/i18n/copy.d.ts +15 -0
  29. package/lib/types/client/i18n/host.d.ts +1 -0
  30. package/lib/types/client/i18n/settings.d.ts +16 -0
  31. package/lib/types/client/settings/card.d.ts +9 -0
  32. package/lib/types/client/settings/install.d.ts +2 -0
  33. package/lib/types/client.d.ts +2 -2
  34. package/lib/types/datasource/python-options.d.ts +1 -2
  35. package/lib/types/datasource/python.d.ts +2 -2
  36. package/lib/types/disclosure/execution-guidance.d.ts +8 -0
  37. package/lib/types/plugin-agents.d.ts +2 -0
  38. package/lib/types/plugin.d.ts +23 -3
  39. package/lib/types/presentation/execution-policy.d.ts +5 -0
  40. package/lib/types/rpc.d.ts +2 -2
  41. package/lib/types/semantic-reference/rpc.d.ts +2 -2
  42. package/lib/types/settings.d.ts +14 -0
  43. package/package.json +85 -65
  44. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  45. package/skills/dsh-data-analysis-files/SKILL.md +22 -10
  46. package/skills/dsh-data-analysis-files/references/delivery.md +15 -0
  47. package/skills/dsh-data-analysis-presentation/SKILL.md +15 -19
  48. package/skills/dsh-data-analysis-presentation/references/schema.md +4 -1
package/README.en.md ADDED
@@ -0,0 +1,138 @@
1
+ # @chengxianglibra/dsh-data-analysis
2
+
3
+ [简体中文](README.md) | English
4
+
5
+ Analyze data with natural language in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
6
+ Explore and reuse business metric definitions, then turn your findings into interactive charts, reports,
7
+ and dashboards that you can also read offline. Semantic analysis is powered by [Marivo](https://github.com/chengxianglibra/marivo).
8
+
9
+ This is an independently maintained community plugin. It is not an official DeepSeek product.
10
+
11
+ ## Capabilities and features
12
+
13
+ | What you want to do | What the plugin offers |
14
+ | --- | --- |
15
+ | Answer business questions | Analyze metric trends, compare periods or business segments, and explore reasons for changes using natural language |
16
+ | Analyze local files | Upload files or use files in your workspace to analyze data and create charts and reports |
17
+ | Connect to data | Configure data sources, manage credentials, and test connections; DuckDB, Trino, and ClickHouse are supported by default |
18
+ | Keep definitions consistent | Work with the assistant to define and reuse metrics and dimensions, and explore their relationships |
19
+ | Specify what to analyze | Search for and reference existing semantic objects with `@` in the chat input |
20
+ | Present findings | Create reports and dashboards with conclusions, metric cards, charts, tables, and source information |
21
+ | Explore further | Filter interactively, adjust the presentation, and reference report content in follow-up questions |
22
+ | Read and share | Save and browse previous versions, and download HTML for offline reading and sharing |
23
+
24
+ ## Installation
25
+
26
+ ### Prerequisites
27
+
28
+ - Node.js `^22.19.0 || >=24.0.0`: version 22.19.0 or later in the 22.x series, or version 24.0.0 and above. Version 23.x is not supported.
29
+ - DeepSeek Harness `>=0.1.7-rc.1`, launched with `npx @deepseek-ai/dsh web` and configured.
30
+ - `pnpm` available on the command line for Harness to install the plugin.
31
+ - Python 3.10+, available as `python3` (`python` on Windows), with `venv`/`ensurepip` support. Some Linux distributions require the additional `python3-venv` package.
32
+
33
+ You do not need to install Marivo beforehand. The analysis environment is prepared automatically on first launch.
34
+ An internet connection is required to download dependencies; wait for setup to finish.
35
+
36
+ ### Install and launch
37
+
38
+ The commands below run Harness through `npx`; no global installation or `dsh` command on PATH is required.
39
+
40
+ Install the plugin into the Web Profile:
41
+
42
+ ```bash
43
+ npx @deepseek-ai/dsh plugin --profile web add @chengxianglibra/dsh-data-analysis
44
+ ```
45
+
46
+ Start the Web Profile after installation. If it is already running, exit and restart it:
47
+
48
+ ```bash
49
+ npx @deepseek-ai/dsh web
50
+ ```
51
+
52
+ In the Web interface, select a Workspace, create a session, and send a message.
53
+ Data Sources (数据源), Semantic Layer (语义层), and Reports (报告) appear beside the session title and open in tabs on the right.
54
+ The examples below use the Web interface; Chinese labels are included to help you locate the controls.
55
+
56
+ ## How to use
57
+
58
+ ### 1. Prepare your data
59
+
60
+ **Local files**: Upload a file and ask a question, or specify a file already in your workspace.
61
+ No data source configuration or business definitions are required first.
62
+ CSV, JSON, Parquet, and Excel `.xlsx` files are supported. Reading `.xlsx` for the first time requires an internet connection to download an extension.
63
+ Convert older `.xls` files to `.xlsx`, CSV, or Parquet first.
64
+ To view or deliver a CSV, TSV, or XLSX file, ask the assistant to use native `present`, then open the read-only DSH spreadsheet preview on the right. Tables with report filtering and linked interactions remain in the report reader. The plugin does not write XLSX or analyze older `.xls` files.
65
+
66
+ **Databases**: Open Data Sources (数据源) beside the session title to view connections and their configuration status in the current workspace:
67
+
68
+ 1. Select Add Data Source (新增数据源), choose an engine, and enter a name and connection details.
69
+ 2. If authentication is required, enter the username, password, or token under Connection Credentials (连接凭证).
70
+ 3. Save and test the connection. Use the result to check the address, credentials, or access permissions.
71
+
72
+ Use Edit Configuration (编辑配置) to update an existing connection. Its name and engine stay fixed; test the connection again after saving.
73
+ Saved credential values are not displayed; leave them blank to keep using them. Submit passwords and tokens through the credential form, not in chat messages.
74
+
75
+ If a data source needs to be added or repaired during analysis, the assistant can open the configuration page.
76
+ Enter a new connection or choose Use Existing Data Source (使用已有数据源), then select Save and Test, Continue on Success (保存并测试,成功后继续).
77
+ The assistant resumes analysis after verifying the connection and access to the target data.
78
+
79
+ ### 2. Ask questions in natural language
80
+
81
+ Describe the data, time range, metric definitions, and results you want. For example:
82
+
83
+ > Using the order data, analyze revenue and order count for August 2026, compare them with July, break them down by channel, and explain the sources and metric definitions.
84
+
85
+ > Check query duration over the last seven days. Identify the slowest query types and provide a trend chart and a detailed table.
86
+
87
+ If you have not yet defined your business metrics, start by asking the assistant to help:
88
+
89
+ > First, find order-related tables in the current data source. Confirm the definitions of revenue and paid order count with me, then save them as reusable business definitions.
90
+
91
+ When you need a report, describe its contents:
92
+
93
+ > Turn this analysis into a report with conclusions, key metrics, trend charts, a channel comparison table, and source information. Provide a downloadable HTML file.
94
+
95
+ ### 3. Browse and reference business definitions
96
+
97
+ Open Semantic Layer (语义层) to find metrics, dimensions, and other objects by category or keyword,
98
+ and inspect their definitions and relationships. Open objects separately to compare them side by side.
99
+
100
+ Type `@` in the chat input to search for and select objects in the current workspace.
101
+ For a multiword search, use `@"monthly revenue"`. Ask questions about the selected objects to keep the analysis aligned with your business definitions.
102
+
103
+ ### 4. Read, edit, and ask follow-up questions about reports
104
+
105
+ When the current session delivers a new report, it opens automatically on the right.
106
+ Use the Reports (报告) catalog to find reports by title and read different reports or versions side by side.
107
+ The page notifies you when a new version is available, so you can choose when to switch.
108
+ You can also read workspace reports from the Reports sidebar entry without an active session.
109
+
110
+ - **Explore data**: Filter charts and tables, and adjust supported chart types and displayed fields. Interactions use data already saved in the report; ask in chat to fetch fresh data or recalculate results.
111
+ - **Check sources**: Inspect the source information attached to report content to review the basis of the analysis.
112
+ - **Edit the presentation**: On the latest version page, select Edit Report (编辑报告) to change titles, text, metric labels, and chart or table settings. Move or remove content, undo or redo changes, and save your edits.
113
+ - **Ask follow-up questions**: Choose Add to Question (加入提问) from a supported report content menu to insert a reference into the chat input, then add your question and send it.
114
+ - **Browse history**: After saving edits, use the history sidebar to view and download saved versions. Historical versions are read-only.
115
+
116
+ Page filters do not recalculate metric cards or change the saved report. To preserve the current filtered view, use Export Current View (导出当前视图).
117
+
118
+ ### 5. Download and share
119
+
120
+ The export menu at the top right of a report offers two options:
121
+
122
+ | Option | Use it to |
123
+ | --- | --- |
124
+ | Download Full Report (下载完整报告) | Download the complete HTML report for the version you are viewing and read it offline |
125
+ | Export Current View (导出当前视图) | Capture the current filters, charts, and table sorting in HTML, with the source version and export time, to share what you see |
126
+
127
+ You can also ask the assistant to export a report as an HTML file. Open downloaded files in a browser to read and interact with them offline, without connecting to the original data source.
128
+ If you are editing a report, save or cancel your edits before exporting.
129
+
130
+ ## Analysis timeout
131
+
132
+ Go to Settings → Plugins → Plugin Configuration → Data Analysis (设置 → 插件 → 插件配置 → 数据分析)
133
+ to adjust the default timeout for Python analysis. The default is 120 seconds, and changes apply to the next analysis call after saving.
134
+ The actual execution limit is still subject to Harness execution policies.
135
+
136
+ ## License
137
+
138
+ This project is licensed under the [MIT License](LICENSE).
package/README.md CHANGED
@@ -1,100 +1,76 @@
1
1
  # @chengxianglibra/dsh-data-analysis
2
2
 
3
+ 简体中文 | [English](README.en.md)
4
+
3
5
  在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 中,用自然语言完成数据分析,
4
6
  浏览和复用业务指标定义,并将结果整理为可交互、可离线阅读的图表、报告和看板。
5
- 语义分析能力由 [Marivo](https://github.com/chengxianglibra/marivo) 提供;本地文件也可直接使用 pandas 或 DuckDB。
7
+ 语义分析能力由 [Marivo](https://github.com/chengxianglibra/marivo) 提供。
6
8
 
7
9
  本项目是个人维护的社区插件,并非 DeepSeek 官方发行或维护的软件。
8
10
 
11
+ ## 能力与特性
12
+
13
+ | 你想做什么 | 插件提供的能力 |
14
+ | --- | --- |
15
+ | 回答业务问题 | 用自然语言分析指标趋势、比较不同时间或业务分组、探索变化原因 |
16
+ | 分析本地文件 | 上传文件或指定工作区中的文件,直接分析并生成图表、报告 |
17
+ | 连接数据 | 配置数据源、管理连接凭证并测试连接;默认支持 DuckDB、Trino、ClickHouse |
18
+ | 统一分析口径 | 与助手一起整理和复用指标、维度等业务定义,浏览对象及其关系 |
19
+ | 明确分析对象 | 在输入框通过 `@` 搜索并引用已有语义对象 |
20
+ | 展示分析结果 | 生成包含结论、指标卡、图表、表格和来源说明的报告或看板 |
21
+ | 继续探索 | 交互筛选、调整呈现,并引用报告内容继续提问 |
22
+ | 阅读与分享 | 保存和查看历史版本,下载 HTML 供离线阅读与分享 |
23
+
9
24
  ## 安装
10
25
 
11
26
  ### 环境准备
12
27
 
13
- - Node.js `^22.19.0 || >=24.0.0`,与 DeepSeek Harness 一致:22.x 需至少 22.19.0,或使用 24.0.0 及以上版本;不支持 23.x。
14
- - 已安装并配置好 DeepSeek Harness `>=0.1.5-rc.1`(rc.1 验收状态见验证指南;范围内未来版本不视为已验收)。
15
- - `pnpm` 可在命令行中使用,供 Harness 安装插件。
16
- - 已安装 Python 3.10+,命令行可运行 `python3`(Windows 为 `python`),且支持 `venv`/`ensurepip`;部分 Linux 发行版需额外安装 `python3-venv`。
28
+ - Node.js `^22.19.0 || >=24.0.0`:22.x 需至少 22.19.0,或使用 24.0.0 及以上版本;不支持 23.x。
29
+ - 已通过 `npx @deepseek-ai/dsh web` 启动并配置好 DeepSeek Harness `>=0.1.7-rc.1`。
30
+ - 命令行中可使用 `pnpm`,供 Harness 安装插件。
31
+ - Python 3.10+,命令行可运行 `python3`(Windows 为 `python`),且支持 `venv`/`ensurepip`;部分 Linux 发行版需额外安装 `python3-venv`。
17
32
 
18
- 默认安装方式会在首次启动时使用本机 Python 创建独立的虚拟环境,并通过 pip 安装 Marivo,无需预装 Marivo 或 uv。
19
- 首次启动需要联网下载依赖,请等待环境准备完成。
33
+ 无需预装 Marivo。首次启动会自动准备分析环境,需要联网下载依赖,请等待准备完成。
20
34
 
21
35
  ### 安装并启动
22
36
 
37
+ 以下命令均通过 `npx` 运行 Harness,无需全局安装或将 `dsh` 添加到 PATH。
38
+
23
39
  将插件安装到 Web Profile:
24
40
 
25
41
  ```bash
26
- dsh plugin --profile web add @chengxianglibra/dsh-data-analysis
42
+ npx @deepseek-ai/dsh plugin --profile web add @chengxianglibra/dsh-data-analysis
27
43
  ```
28
44
 
29
45
  安装后启动 Web Profile;如果已在运行,请先退出再重新启动:
30
46
 
31
47
  ```bash
32
- dsh --profile web
48
+ npx @deepseek-ai/dsh web
33
49
  ```
34
50
 
35
51
  在 Web 界面选择分析使用的工作区(Workspace),新建会话并发送一条消息。
36
- 会话标题旁按顺序显示“数据源”“语义层”“报告”三个入口,浏览页面在原生右侧标签页打开。本文以 Web 界面为例。
37
-
38
- ### 本地文件分析
39
-
40
- 上传文件后直接提问,或指定 Workspace 中已有文件。助手可在插件绑定的 Python 环境中使用 pandas
41
- 或原生 DuckDB 分析 CSV、JSON、Parquet,无需先配置数据源或建立语义模型;需要时可继续生成报告。
42
- 默认安装包含 Marivo 的 `duckdb` extra(同时保留 `trino`、`clickhouse`)。Excel `.xlsx` 通过 DuckDB 的
43
- [excel 扩展](https://duckdb.org/docs/stable/core_extensions/excel.html)读取;首次使用需要联网下载扩展,
44
- 并允许写入 DuckDB 扩展缓存。它不是额外的 Python 包,无需为此安装 `openpyxl`。
45
- 旧版 `.xls` 不在该扩展支持范围内,需要先转换为 `.xlsx`、CSV 或 Parquet。
46
-
47
- 纯文件分析无需经过 Marivo `md.raw_sql`。下面的数据源配置流程适用于数据库连接和需要复用的 Marivo 数据源。
48
-
49
- ## 能力
50
-
51
- | 你想做什么 | 插件提供的能力 |
52
- | --- | --- |
53
- | 回答业务问题 | 用自然语言提出问题,分析指标趋势、比较不同时间或业务分组、探索变化原因 |
54
- | 分析本地文件 | 复用原生附件路径,以 pandas 或 DuckDB 直接计算,并按需交付图表或报告 |
55
- | 连接数据 | 配置数据源、管理连接凭证并测试连接;默认包含 DuckDB、Trino、ClickHouse 支持 |
56
- | 统一分析口径 | 与 Agent 一起整理和复用指标、维度等业务定义,浏览语义对象及其关系 |
57
- | 明确分析对象 | 在输入框通过 `@` 搜索并引用已有语义对象 |
58
- | 展示分析结果 | 生成包含文字、指标卡、图表、表格和来源说明的报告或看板 |
59
- | 阅读与分享 | 交互筛选、调整呈现、保存版本,并下载 HTML 离线阅读 |
52
+ 会话标题旁会显示“数据源”“语义层”“报告”三个入口,相关页面在右侧标签页打开。以下以 Web 界面为例。
60
53
 
61
54
  ## 如何使用
62
55
 
63
- ### 1. 准备数据源
64
-
65
- 在会话标题旁打开“数据源”,查看所属工作区的连接属性与配置状态:
66
-
67
- 1. 点击“新增数据源”,选择引擎,按表单填写名称及连接信息。
68
- 2. 如果需要身份认证,在同一表单的“连接凭证”中直接填写用户名、密码或令牌。
69
- 3. 测试连接,根据结果检查地址、凭证或访问权限。
70
-
71
- 凭证引用名自动生成,可展开“凭证引用名 · 确认或修改”查看和调整;也可填写已有引用,并将值留空以复用凭证。
72
- 编辑时已保存的凭证值不回显,留空沿用;填写新值默认使用新的引用,不删除旧的共享凭证。
73
- 手动指定已存在的引用并填写新值时,不会自动覆盖,需到凭证页确认更新。
74
- 已有数据源可点击“编辑配置”修改连接信息,名称和引擎保持不变;保存后重新测试。
56
+ ### 1. 准备数据
75
57
 
76
- 管理员可在 Harness 当前 profile 的本插件配置项下设置 `datasourceDefaults`,按引擎类型为新建表单填入默认值:
58
+ **本地文件**:上传文件后直接提问,或指定工作区中已有文件,无需先配置数据源或建立业务定义。
59
+ 支持 CSV、JSON、Parquet 和 Excel `.xlsx`;首次读取 `.xlsx` 需要联网下载扩展。
60
+ 旧版 `.xls` 请先转换为 `.xlsx`、CSV 或 Parquet。
61
+ 需要查看或交付 CSV、TSV、XLSX 文件时,可让助手通过原生 `present` 交付,随后在右侧打开 DSH 的只读表格预览;报告中可筛选联动的表格仍使用报告阅读器。插件不提供 XLSX 写入或旧 `.xls` 分析。
77
62
 
78
- ```yaml
79
- datasourceDefaults:
80
- trino:
81
- host: trino.internal.example
82
- port: 443
83
- ```
63
+ **数据库**:在会话标题旁打开“数据源”,查看当前工作区的连接与配置状态:
84
64
 
85
- 这是插件配置片段,随 Harness 现有配置加载机制生效;不是独立配置文件。未指定的字段保持原样,用户可修改或清空填充值;
86
- 切换引擎时清空表单并应用目标引擎的默认值。编辑或复用已有数据源不应用这些默认值。
87
- 只配置非敏感普通字段,不能设置 `*_env` 凭据引用或凭据值;内部地址只保存在部署环境的配置中,不必加入插件源码。
88
- 打开新建表单时按当前 Runtime 校验引擎、字段和类型,错误会明确显示且不回显配置值;配置错误不阻止已有数据源编辑。
65
+ 1. 点击“新增数据源”,选择引擎,填写名称及连接信息。
66
+ 2. 如果需要身份认证,在“连接凭证”中填写用户名、密码或令牌。
67
+ 3. 保存并测试连接,根据结果检查地址、凭证或访问权限。
89
68
 
90
- 分析需要新增或修复数据源时,助手可自动打开所属会话的右侧配置页。填写后点击“保存并测试,成功后继续”;
91
- 缺少凭证时继续在凭证表单补齐,测试成功后原调用返回,助手再核验目标表并继续分析。
92
- 请求页默认显示新增表单;切换“使用已有数据源”后,可选择已有连接并验证,无需重复创建。测试失败保留已保存的配置,允许修正后重试;取消请求不会作为成功继续。
69
+ 已有数据源可通过“编辑配置”修改连接信息,名称和引擎保持不变;保存后重新测试。
70
+ 已保存的凭证值不会回显,留空可沿用。请通过凭证表单提交密码或令牌,不要粘贴到聊天消息中。
93
71
 
94
- 更新插件后,如果旧浏览器页面中工具正在等待、却没有打开配置 Tab,也没有“待配置”入口,
95
- 先刷新浏览器页面以加载当前客户端。仍存活的原请求会重新显示;刷新页面不等于重启 Host。
96
- 若原调用已取消或 Host 已重启,需要重新发起配置请求。
97
- 请通过凭证表单提交密码或令牌,不要粘贴到聊天消息中。
72
+ 分析过程中需要新增或修复数据源时,助手可打开配置页。你可以填写新连接,或选择“使用已有数据源”,
73
+ 点击“保存并测试,成功后继续”,连接与目标数据访问验证通过后,助手会继续分析。
98
74
 
99
75
  ### 2. 用自然语言提出分析问题
100
76
 
@@ -104,7 +80,7 @@ datasourceDefaults:
104
80
 
105
81
  > 检查最近七天的查询耗时,找出耗时最高的查询类型,给出趋势图和明细表。
106
82
 
107
- 如果还没有整理业务定义,可以先让 Agent 帮助梳理:
83
+ 如果还没有整理业务定义,可以先让助手帮助梳理:
108
84
 
109
85
  > 先查看当前数据源中有哪些订单相关表,和我确认销售额、支付订单数的统计口径,再保存为可复用的业务定义。
110
86
 
@@ -114,26 +90,21 @@ datasourceDefaults:
114
90
 
115
91
  ### 3. 浏览和引用业务定义
116
92
 
117
- 点击会话标题旁的“语义层”,按分类或关键词查找指标、维度等对象,查看业务定义和对象关系。
118
- 这里展示已有定义;浏览页面不会执行分析查询。浏览页固定属于入口所在会话的工作区,支持把语义对象独立打开。
93
+ 点击“语义层”,按分类或关键词查找指标、维度等对象,查看业务定义和对象关系。
94
+ 支持独立打开对象,方便对照阅读。
119
95
 
120
- 在聊天输入框输入 `@` 可搜索并选择当前工作区的对象;多词搜索可输入 `@"monthly revenue"`。
121
- 选中后将显示完整的 `kind:path` 引用,便于 Agent 使用你指定的对象开展分析。
96
+ 在聊天输入框输入 `@`,搜索并选择当前工作区的对象;多词搜索可输入 `@"monthly revenue"`。
97
+ 选中后即可围绕指定对象提问,让助手使用一致的业务口径开展分析。
122
98
 
123
99
  ### 4. 阅读、调整和追问报告
124
100
 
125
- 当前会话交付新报告后,右侧自动打开该次交付的固定版本;后台完成、历史回放和重连不会抢占阅读页面。
126
- 同工作区的后台交付会更新已打开的报告目录,并在 current 页提示新版本,保留正在阅读的内容。
127
- 报告生成后自动在右侧 tab 打开固定版本,主对话不附加报告 block。默认只保存 JSON,点击下载或显式调用导出工具时才生成 HTML。“报告”目录按标题查找并打开 current;可以并排阅读不同报告和版本。
128
-
129
- 用户要求离线 HTML 文件时,DSH 可直接调用 `marivo_export_html({ report_id, build_id?, output_path })`,例如输出到 `reports/analysis.html`。省略 `build_id` 时固定调用开始时的 current;指定时导出该历史版本。输出是完整、自包含且可交互的报告,不含未保存编辑或临时筛选。路径必须位于当前 Workspace,已有文件不会覆盖;导出不需要 Python、数据源或发布配置。成功返回实际路径、Build ID、字节数和 SHA-256;DSH 随后通过原生 `present({ files: [{ path, description }] })` 交付该文件。
130
- 发现新版本时 current 先提示,点击刷新或重新打开才切换,旧版本筛选随切换重置。
131
- 没有活动会话时,侧栏的“报告”入口仍可阅读 Workspace 中的报告。
101
+ 当前会话交付新报告后,会自动在右侧打开。通过“报告”目录可按标题查找报告,并排阅读不同报告和版本。
102
+ 发现新版本时,页面会提示更新,你可以选择何时切换。没有活动会话时,也可通过侧栏的“报告”入口阅读工作区中的报告。
132
103
 
133
104
  - **探索数据**:筛选图表和表格,调整支持的图形及展示字段。交互使用报告中已保存的数据;获取新数据或重新计算时,在会话中提出要求。
134
105
  - **查看来源**:查看报告内容附带的来源信息,核对分析依据。
135
- - **调整呈现**:从 current 页点击“编辑报告”,在原编辑窗口编辑标题、正文、指标标签和图表/表格配置,移动或删除内容,支持撤销、重做和保存。
136
- - **继续追问**:在支持的报告内容菜单中选择“加入提问”,在输入框插入 `# <cell名称>` 引用标签,补充问题后发送;完整上下文会随引用一起提交。
106
+ - **调整呈现**:在最新版本页面点击“编辑报告”,编辑标题、正文、指标标签和图表/表格配置,移动或删除内容,支持撤销、重做和保存。
107
+ - **继续追问**:在支持的报告内容菜单中选择“加入提问”,将相关内容引用到输入框,补充问题后发送。
137
108
  - **查看历史**:保存编辑后可在历史侧栏查看和下载已保存的版本,历史版本只读。
138
109
 
139
110
  页面筛选不会重新计算指标卡,也不会写入保存的报告。希望保留当前筛选结果时,使用“导出当前视图”。
@@ -147,12 +118,14 @@ datasourceDefaults:
147
118
  | 下载完整报告 | 下载所查看版本的完整 HTML 报告,供离线阅读 |
148
119
  | 导出当前视图 | 将当前筛选、图形和表格排序固定为 HTML,附带来源版本及导出时间,分享当前看到的内容 |
149
120
 
150
- 编辑模式下,请先保存或取消编辑再导出。下载的文件可直接用浏览器打开。
121
+ 也可以直接让助手将报告导出为 HTML 文件。下载的文件可用浏览器打开,离线阅读和交互,无需连接原始数据源。
122
+ 编辑模式下,请先保存或取消编辑再导出。
151
123
 
152
- ## 许可证
124
+ ## 分析超时设置
153
125
 
154
- 本项目采用 [MIT License](LICENSE)。
126
+ 在“设置 → 插件 → 插件配置 → 数据分析”中可调整 Python 分析的默认超时时间,默认为 120 秒,
127
+ 保存后对下一次分析调用生效。实际执行时限仍受 Harness 执行策略约束。
155
128
 
156
- ## 报告发布到对象存储
129
+ ## 许可证
157
130
 
158
- 通过 `reportPublishing.enabled` 开启 S3 兼容对象存储发布;在“数据源与凭证”管理独立 AK/SK 和可选 Token。开启后报告菜单的完整 HTML 与当前视图下载均替换为发布,成功返回链接。配置示例与边界见[报告 HTML 发布](../../docs/modules/report-publishing.md)。
131
+ 本项目采用 [MIT License](LICENSE)。
@@ -23,7 +23,7 @@ export function TestResult({ result, stale = false }) {
23
23
  const t = useCopy();
24
24
  if (!result)
25
25
  return null;
26
- return (_jsxs("div", { className: "mc-result", "data-tone": t(stale ? 'stale' : result.ok ? 'success' : 'error'), role: "status", children: [_jsxs("div", { className: "mc-result-title", children: [_jsx(CredentialIcon, { name: stale || !result.ok ? 'info' : 'check', size: 16 }), stale
26
+ return (_jsxs("div", { className: "mc-result", "data-tone": stale ? 'stale' : result.ok ? 'success' : 'error', role: "status", children: [_jsxs("div", { className: "mc-result-title", children: [_jsx(CredentialIcon, { name: stale || !result.ok ? 'info' : 'check', size: 16 }), stale
27
27
  ? t('marivo.credentials.configuration-changed-test-again')
28
28
  : result.ok
29
29
  ? t('marivo.credentials.connection-test-succeeded')
@@ -47,7 +47,7 @@ function OperationOutcome({ entry }) {
47
47
  operation?.result?.ok === false;
48
48
  if (!unsuccessful && operation?.action !== 'delete')
49
49
  return null;
50
- return (_jsxs("div", { className: "mc-operation-note", "data-error": t(Boolean(entry.error || errors.length)), role: "status", children: [entry.error && _jsx("p", { children: t(entry.error) }), operation?.status === 'cancelled' && (_jsx("p", { children: t('marivo.credentials.this-operation-was-cancelled') })), operation?.status === 'succeeded' && operation.action === 'delete' && (_jsx("p", { children: t('marivo.credentials.saved-value-deleted-configuration-status-updated') })), errors.length > 0 && _jsx("p", { children: errors.map(credentialMessage).join(' ') }), operation?.status === 'failed' && errors.length === 0 && !operation.result && (_jsx("p", { children: t('marivo.credentials.operation-incomplete-please-retry') })), unsuccessful && operation?.saved.length > 0 && (_jsxs("p", { children: [t('marivo.credentials.saved'), operation.saved.join('、'), "\u3002"] }))] }));
50
+ return (_jsxs("div", { className: "mc-operation-note", "data-error": Boolean(entry.error || errors.length), role: "status", children: [entry.error && _jsx("p", { children: t(entry.error) }), operation?.status === 'cancelled' && (_jsx("p", { children: t('marivo.credentials.this-operation-was-cancelled') })), operation?.status === 'succeeded' && operation.action === 'delete' && (_jsx("p", { children: t('marivo.credentials.saved-value-deleted-configuration-status-updated') })), errors.length > 0 && _jsx("p", { children: errors.map(credentialMessage).join(' ') }), operation?.status === 'failed' && errors.length === 0 && !operation.result && (_jsx("p", { children: t('marivo.credentials.operation-incomplete-please-retry') })), unsuccessful && operation?.saved.length > 0 && (_jsxs("p", { children: [t('marivo.credentials.saved'), operation.saved.join('、'), "\u3002"] }))] }));
51
51
  }
52
52
  export function DatasourceProperties({ context }) {
53
53
  const t = useCopy();
@@ -307,7 +307,7 @@ export function installCredentials(ctx, rpc, changes) {
307
307
  : t('marivo.credentials.awaiting-credentials'), type: "button", onClick: () => model.openRequest(request.id), children: t('marivo.credentials.configuration-needed') })) : null;
308
308
  })));
309
309
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ locale: 'marivo.navigation', name: 'shell.overlay', id: 'marivo-credential-observer' }, localized(ctx, function Observer({ useSessions, useWorkspaces }) {
310
- const sessionId = useSessions((state) => state.current) ?? '';
310
+ const sessionId = useSessions((state) => state.ids.find((id) => state.byId[id]?.retainedBy.mainView)) ?? '';
311
311
  const workspaces = useWorkspaces((state) => state.items);
312
312
  const currentWorkspace = workspaces.find((item) => item.sessionIds.includes(sessionId))?.workspaceId ?? '';
313
313
  useEffect(() => model.session(sessionId), [sessionId]);
@@ -3,13 +3,15 @@ import * as navigation from "./navigation.js";
3
3
  import * as presentation from "./presentation.js";
4
4
  import * as python from "./python.js";
5
5
  import * as semantic from "./semantic.js";
6
- export const dictionaries = { credentials, navigation, presentation, semantic, python };
6
+ import * as settings from "./settings.js";
7
+ export const dictionaries = { credentials, navigation, presentation, semantic, python, settings };
7
8
  export const zh = {
8
9
  ...credentials.zh,
9
10
  ...navigation.zh,
10
11
  ...presentation.zh,
11
12
  ...semantic.zh,
12
13
  ...python.zh,
14
+ ...settings.zh,
13
15
  };
14
16
  export const en = {
15
17
  ...credentials.en,
@@ -17,6 +19,7 @@ export const en = {
17
19
  ...presentation.en,
18
20
  ...semantic.en,
19
21
  ...python.en,
22
+ ...settings.en,
20
23
  };
21
24
  export function message(key, params = {}) {
22
25
  return { key, params };
@@ -8,6 +8,7 @@ export function installCopy(ctx) {
8
8
  ctx.effect(() => ctx.locale.register('marivo.presentation', dictionaries.presentation));
9
9
  ctx.effect(() => ctx.locale.register('marivo.semantic', dictionaries.semantic));
10
10
  ctx.effect(() => ctx.locale.register('marivo.python', dictionaries.python));
11
+ ctx.effect(() => ctx.locale.register('marivo.settings', dictionaries.settings));
11
12
  }
12
13
  export function hostCopy(ctx) {
13
14
  const active = ctx.locale.getSnapshot().active;
@@ -0,0 +1,30 @@
1
+ export const zh = {
2
+ 'marivo.settings.title': '数据分析',
3
+ 'marivo.settings.timeout': 'Python 默认执行超时(秒)',
4
+ 'marivo.settings.hint': '默认 120 秒。保存后对所有 Workspace 的下一次 Python 调用生效,不改变已开始调用的时限。单次调用可指定其他时限;仍受 Harness 执行限制约束。',
5
+ 'marivo.settings.description': 'Python 执行设置。',
6
+ 'marivo.settings.expand': '展开设置',
7
+ 'marivo.settings.collapse': '收起设置',
8
+ 'marivo.settings.save': '保存',
9
+ 'marivo.settings.saving': '保存中…',
10
+ 'marivo.settings.discard': '放弃修改',
11
+ 'marivo.settings.unsaved': '未保存',
12
+ 'marivo.settings.invalid': '请输入 0.001 至 2147483.647 秒,最多三位小数。',
13
+ 'marivo.settings.read-only': '当前连接的设置只读。',
14
+ 'marivo.settings.failed': '保存失败或设置已被其他页面修改。草稿已保留;可放弃修改以读取最新值,再重试。',
15
+ };
16
+ export const en = {
17
+ 'marivo.settings.title': 'Data analysis',
18
+ 'marivo.settings.timeout': 'Default Python execution timeout (seconds)',
19
+ 'marivo.settings.hint': 'Defaults to 120 seconds. Saved changes apply to the next Python call in every Workspace; calls already started keep their timeout. Individual calls may override it. Harness execution limits still apply.',
20
+ 'marivo.settings.description': 'Python execution settings.',
21
+ 'marivo.settings.expand': 'Expand settings',
22
+ 'marivo.settings.collapse': 'Collapse settings',
23
+ 'marivo.settings.save': 'Save',
24
+ 'marivo.settings.saving': 'Saving…',
25
+ 'marivo.settings.discard': 'Discard changes',
26
+ 'marivo.settings.unsaved': 'Unsaved',
27
+ 'marivo.settings.invalid': 'Enter 0.001–2147483.647 seconds with at most three decimal places.',
28
+ 'marivo.settings.read-only': 'Settings are read-only on this connection.',
29
+ 'marivo.settings.failed': 'Saving failed or another page changed these settings. Your draft is preserved. Discard it to read the latest value, then retry.',
30
+ };
@@ -3,7 +3,7 @@ export function ownedInput(host, sessionId, workspaceId) {
3
3
  const owners = workspaces.items.filter((item) => item.sessionIds.includes(sessionId));
4
4
  if (!sessionId ||
5
5
  !workspaceId ||
6
- host.sessions.list.getSnapshot().current !== sessionId ||
6
+ !host.sessions.list.getSnapshot().byId[sessionId]?.retainedBy.mainView ||
7
7
  workspaces.phase !== 'ready' ||
8
8
  workspaces.state === 'error' ||
9
9
  owners.length !== 1 ||
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { CodeBlock, DisclosureRow, IconCodeOutline16, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { CodeBlock, DisclosureRow, IconCodeOutlineRegular, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';
3
3
  import { useMemo, useState } from 'react';
4
4
  import { useCopy } from "../i18n/context.js";
5
5
  import { pythonToolModel } from "./model.js";
@@ -29,7 +29,7 @@ export function PythonToolCard({ block, inspect }) {
29
29
  const ok = await writeClipboard(code).catch(() => false);
30
30
  setCopy({ code, ok });
31
31
  };
32
- return (_jsxs("div", { className: "mp-tool", "data-state": model.state, children: [_jsx("style", { children: styles }), _jsx(DisclosureRow, { icon: _jsx(IconCodeOutline16, {}), title: "marivo_python", open: open, expandable: true, expandOnRowClick: true, keepContentWhenOpen: true, onToggle: () => setOpen((value) => !value), collapsedContent: _jsxs(_Fragment, { children: [_jsx("span", { className: "mp-summary", children: model.summary }), _jsx("span", { className: "mp-state", children: t(`marivo.python.${model.state}`) })] }), children: open && (_jsxs("div", { className: "mp-body", children: [_jsxs("section", { "aria-label": t('marivo.python.input'), children: [_jsxs("div", { className: "mp-input-heading", children: [_jsx("h4", { children: t('marivo.python.input') }), model.code !== undefined && (_jsx("button", { type: "button", className: "mp-inspect", onClick: () => void copySource(), children: t(copy?.code === model.code && copy.ok
32
+ return (_jsxs("div", { className: "mp-tool", "data-state": model.state, children: [_jsx("style", { children: styles }), _jsx(DisclosureRow, { icon: _jsx(IconCodeOutlineRegular, { size: 16 }), title: "marivo_python", open: open, expandable: true, expandOnRowClick: true, keepContentWhenOpen: true, onToggle: () => setOpen((value) => !value), collapsedContent: _jsxs(_Fragment, { children: [_jsx("span", { className: "mp-summary", children: model.summary }), _jsx("span", { className: "mp-state", children: t(`marivo.python.${model.state}`) })] }), children: open && (_jsxs("div", { className: "mp-body", children: [_jsxs("section", { "aria-label": t('marivo.python.input'), children: [_jsxs("div", { className: "mp-input-heading", children: [_jsx("h4", { children: t('marivo.python.input') }), model.code !== undefined && (_jsx("button", { type: "button", className: "mp-inspect", onClick: () => void copySource(), children: t(copy?.code === model.code && copy.ok
33
33
  ? 'marivo.python.copied'
34
34
  : 'marivo.python.copy') }))] }), copy && copy.code === model.code && !copy.ok && (_jsx("p", { role: "status", className: "mp-notice", children: t('marivo.python.copy-failed') })), _jsx("div", { className: "mp-scroll", children: model.code === undefined ? (_jsx("pre", { children: model.input })) : (_jsx(CodeBlock, { code: `${model.code}\n`, lang: "python", className: "mp-code", copyLabel: t('marivo.python.copy'), copiedLabel: t('marivo.python.copied') })) })] }), _jsxs("dl", { className: "mp-facts", children: [model.datasources !== undefined && (_jsxs("div", { children: [_jsx("dt", { children: "datasources" }), _jsx("dd", { children: JSON.stringify(model.datasources) })] })), model.timeoutMs !== undefined && (_jsxs("div", { children: [_jsx("dt", { children: "timeoutMs" }), _jsxs("dd", { children: [model.timeoutMs, " ms"] })] })), model.facts.map(({ name, value }) => (_jsxs("div", { children: [_jsx("dt", { children: name }), _jsx("dd", { children: value })] }, name)))] }), model.notices.map((notice) => (_jsx("p", { className: "mp-notice", children: notice }, notice))), model.facts.some((fact) => fact.name === 'truncated' && fact.value === 'true') && (_jsx("p", { className: "mp-notice", children: t('marivo.python.truncated') })), ['stdout', 'stderr'].map((name) => model[name] === undefined ? null : (_jsxs("section", { "aria-label": name, children: [_jsx("h4", { children: name }), _jsx("pre", { className: "mp-scroll", children: model[name] })] }, name))), model.fallback !== undefined && (_jsxs("section", { "aria-label": t('marivo.python.output'), children: [_jsx("h4", { children: t('marivo.python.output') }), _jsx("pre", { className: "mp-scroll", children: model.fallback })] })), _jsxs("details", { children: [_jsx("summary", { children: t('marivo.python.raw') }), _jsx("h4", { children: t('marivo.python.input') }), _jsx("pre", { className: "mp-scroll", children: model.input }), model.output !== undefined && (_jsxs(_Fragment, { children: [_jsx("h4", { children: t('marivo.python.output') }), _jsx("pre", { className: "mp-scroll", children: model.output })] }))] }), inspect && (_jsx("div", { children: _jsx("button", { type: "button", className: "mp-inspect", onClick: inspect, children: t('marivo.python.inspect') }) }))] })) })] }));
35
35
  }
@@ -15,7 +15,7 @@ function parse(text) {
15
15
  /** Read only the frozen Harness call; never fetch results or infer execution from settlement. */
16
16
  export function pythonToolModel(block) {
17
17
  const done = 'kind' in block;
18
- const input = (done ? block.call?.argsRaw : block.argsRaw) ?? '';
18
+ const input = (done ? block.call?.argsRaw : block.phase === 'start' ? block.argsRaw : undefined) ?? '';
19
19
  const args = parse(input);
20
20
  const code = typeof args?.code === 'string' ? args.code : undefined;
21
21
  const output = done
@@ -31,6 +31,7 @@ export const inject = [
31
31
  'conversation',
32
32
  'sidebarRight',
33
33
  'sidebarRightTabs',
34
+ 'uiWorkspace',
34
35
  ];
35
36
  const labels = workspaceDirectories;
36
37
  /** Default browser integration. Layout belongs to Harness; mutable content belongs to occurrences. */
@@ -78,7 +79,7 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
78
79
  state.phase !== 'ready' ||
79
80
  state.state === 'error' ||
80
81
  workspaceFor(sessionId) !== workspaceId ||
81
- (foreground && ctx.sessions.list.getSnapshot().current !== sessionId))
82
+ (foreground && !ctx.sessions.list.getSnapshot().byId[sessionId]?.retainedBy.mainView))
82
83
  throw new Error('marivo.navigation.the-owning-session-or-workspace-changed-return-to-its');
83
84
  };
84
85
  const changed = (workspaceId, reportId) => {
@@ -213,7 +214,7 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
213
214
  }),
214
215
  };
215
216
  return (_jsxs("section", { className: "rt-page", "data-rt-tab": tab.id, "data-rt-panel": panelId, "data-rt-kind": page.target.kind, "data-rt-workspace": page.target.workspaceId, "data-rt-build": report.document?.buildId, "data-rt-current": page.target.kind === 'report' && !page.target.buildId ? 'true' : undefined, children: [_jsx("style", { children: catalogStyles + credentialStyles + rightTabStyles }), state.error ? (_jsx("p", { role: "alert", children: t(state.error) })) : (_jsxs(_Fragment, { children: [page.target.kind === 'reports' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "rt-heading-row", children: [_jsx("h2", { className: "rt-heading", children: t(labels[page.target.kind]) }), page.target.kind === 'reports' && (_jsx("button", { type: "button", disabled: catalog.loading, onClick: () => void page.refresh(), children: t('marivo.presentation.refresh') }))] }), _jsx("p", { className: "rt-caption", children: workspaces.find((w) => w.workspaceId === page.target.workspaceId)?.title ??
216
- page.target.workspaceId })] })), state.newer && (_jsxs("p", { role: "status", className: "rt-notice", children: [t('marivo.navigation.a-newer-version-exists-the-current-view-is-unchanged'), _jsx("button", { type: "button", onClick: () => void page.refresh(), children: t('marivo.navigation.view-new-version') })] })), state.notice && _jsx("p", { role: "status", children: t(state.notice) }), page.target.kind === 'reports' && (_jsx(ReportCatalogView, { model: page.catalog, reader: catalogReader, sessions: sessions, onOpenSession: (id) => ctx.sessions.open(id) })), page.target.kind === 'semantic' && (_jsx(SemanticBrowserPanel, { model: page.semantic, onAsk: canAsk ? () => page.semantic.addToQuestion(ctx, page.sessionId) : undefined, workspaces: workspaces })), page.target.kind === 'datasources' && (_jsx(CredentialPanel, { model: page.datasources, workspaces: workspaces, onRefresh: () => void page.refresh() })), page.target.kind === 'report' && (_jsxs(_Fragment, { children: [report.editing && (_jsxs("div", { className: "rt-toolbar", role: "toolbar", "aria-label": t('marivo.presentation.report-editing-actions'), children: [_jsx("button", { type: "button", disabled: report.saving || !report.editing.undo.length, onClick: () => page.reader.undoEdit(), children: t('marivo.presentation.undo') }), _jsx("button", { type: "button", disabled: report.saving || !report.editing.redo.length, onClick: () => page.reader.redoEdit(), children: t('marivo.presentation.redo') }), _jsx("button", { type: "button", disabled: report.saving, onClick: () => act(page, () => save(page, tab)), children: report.saving
217
+ page.target.workspaceId })] })), state.newer && (_jsxs("p", { role: "status", className: "rt-notice", children: [t('marivo.navigation.a-newer-version-exists-the-current-view-is-unchanged'), _jsx("button", { type: "button", onClick: () => void page.refresh(), children: t('marivo.navigation.view-new-version') })] })), state.notice && _jsx("p", { role: "status", children: t(state.notice) }), page.target.kind === 'reports' && (_jsx(ReportCatalogView, { model: page.catalog, reader: catalogReader, sessions: sessions, onOpenSession: (id) => ctx.uiWorkspace.openSession(id) })), page.target.kind === 'semantic' && (_jsx(SemanticBrowserPanel, { model: page.semantic, onAsk: canAsk ? () => page.semantic.addToQuestion(ctx, page.sessionId) : undefined, workspaces: workspaces })), page.target.kind === 'datasources' && (_jsx(CredentialPanel, { model: page.datasources, workspaces: workspaces, onRefresh: () => void page.refresh() })), page.target.kind === 'report' && (_jsxs(_Fragment, { children: [report.editing && (_jsxs("div", { className: "rt-toolbar", role: "toolbar", "aria-label": t('marivo.presentation.report-editing-actions'), children: [_jsx("button", { type: "button", disabled: report.saving || !report.editing.undo.length, onClick: () => page.reader.undoEdit(), children: t('marivo.presentation.undo') }), _jsx("button", { type: "button", disabled: report.saving || !report.editing.redo.length, onClick: () => page.reader.redoEdit(), children: t('marivo.presentation.redo') }), _jsx("button", { type: "button", disabled: report.saving, onClick: () => act(page, () => save(page, tab)), children: report.saving
217
218
  ? t('marivo.presentation.saving')
218
219
  : t('marivo.presentation.save-edits') }), _jsx("button", { type: "button", disabled: report.saving, onClick: () => {
219
220
  if (!page.reader.dirty ||
@@ -221,7 +222,7 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
221
222
  page.reader.cancelEdit();
222
223
  }, children: t('marivo.presentation.cancel-edits') }), _jsx("span", { children: page.reader.dirty
223
224
  ? t('marivo.presentation.unsaved-edits')
224
- : t('marivo.presentation.edit-mode') })] })), report.editError && _jsx("p", { role: "alert", children: t(report.editError) }), report.notice && _jsx("p", { role: "status", children: t(report.notice) }), report.publicationUrl && (_jsx("p", { children: _jsx("a", { href: report.publicationUrl, target: "_blank", rel: "noopener noreferrer", children: t('marivo.presentation.open-published-report') }) })), report.loading && _jsx("p", { role: "status", children: t('marivo.navigation.loading-report') }), report.error && (_jsxs("div", { role: "alert", children: [_jsx("p", { children: t(report.error) }), _jsx("button", { type: "button", onClick: () => void page.refresh(), children: t('marivo.navigation.reload-report') })] })), report.historyOpen && (_jsx(ReportHistoryPanel, { state: report, model: historyModel, sessions: sessions, onOpenSession: (id) => ctx.sessions.open(id) })), report.document && (_jsx(HostPresentationReader, { document: report.document, closeSourceOnNavigate: true, viewMemory: page.viewMemory, exportActions: {
225
+ : t('marivo.presentation.edit-mode') })] })), report.editError && _jsx("p", { role: "alert", children: t(report.editError) }), report.notice && _jsx("p", { role: "status", children: t(report.notice) }), report.publicationUrl && (_jsx("p", { children: _jsx("a", { href: report.publicationUrl, target: "_blank", rel: "noopener noreferrer", children: t('marivo.presentation.open-published-report') }) })), report.loading && _jsx("p", { role: "status", children: t('marivo.navigation.loading-report') }), report.error && (_jsxs("div", { role: "alert", children: [_jsx("p", { children: t(report.error) }), _jsx("button", { type: "button", onClick: () => void page.refresh(), children: t('marivo.navigation.reload-report') })] })), report.historyOpen && (_jsx(ReportHistoryPanel, { state: report, model: historyModel, sessions: sessions, onOpenSession: (id) => ctx.uiWorkspace.openSession(id) })), report.document && (_jsx(HostPresentationReader, { document: report.document, closeSourceOnNavigate: true, viewMemory: page.viewMemory, exportActions: {
225
226
  downloadFullReport: () => void page.reader.downloadDisplayed(),
226
227
  publishing: report.publishingName
227
228
  ? {
@@ -257,7 +258,7 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
257
258
  function Body({ sessionId, useTabInfo, useWorkspaces, useSessions }) {
258
259
  const t = useCopy();
259
260
  const { tab, panel } = useTabInfo();
260
- const workspaces = useWorkspaces((s) => s.items), sessions = useSessions((s) => s.byId), currentSession = useSessions((s) => s.current), workspaceReady = useWorkspaces((s) => s.phase === 'ready' && s.state !== 'error');
261
+ const workspaces = useWorkspaces((s) => s.items), sessions = useSessions((s) => s.byId), currentSession = useSessions((s) => s.ids.find((id) => s.byId[id]?.retainedBy.mainView)), workspaceReady = useWorkspaces((s) => s.phase === 'ready' && s.state !== 'error');
261
262
  const [page, setPage] = useState(), [error, setError] = useState('');
262
263
  useEffect(() => {
263
264
  if (tab.signal.aborted)
@@ -383,7 +384,9 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
383
384
  const bind = () => {
384
385
  if (!ctx.connection.generation.getSnapshot())
385
386
  return stopFeeds();
386
- const current = ctx.sessions.list.getSnapshot().current;
387
+ const current = ctx.sessions.list
388
+ .getSnapshot()
389
+ .ids.find((id) => ctx.sessions.list.getSnapshot().byId[id]?.retainedBy.mainView);
387
390
  const workspaces = ctx.workspaces.list.getSnapshot();
388
391
  if (workspaces.phase !== 'ready' || workspaces.state === 'error')
389
392
  return stopFeeds();
@@ -414,14 +417,14 @@ export function installRightTabs(ctx, { diagnostics = false } = {}) {
414
417
  check(id, workspaceId);
415
418
  }
416
419
  catch (error) {
417
- if (ctx.sessions.list.getSnapshot().current === id)
420
+ if (ctx.sessions.list.getSnapshot().byId[id]?.retainedBy.mainView)
418
421
  fail(error);
419
422
  return;
420
423
  }
421
424
  changed(workspaceId, reportId);
422
425
  // Publication invalidation is Workspace-wide. Foreground navigation
423
426
  // alone is gated, and background deliveries are never queued.
424
- if (ctx.sessions.list.getSnapshot().current !== id)
427
+ if (!ctx.sessions.list.getSnapshot().byId[id]?.retainedBy.mainView)
425
428
  return;
426
429
  try {
427
430
  navigate(id, { kind: 'report', workspaceId, reportId, buildId }, ctx.sidebarRight, true);