dsh-db-studio 1.0.1 → 1.0.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.0.3 — 2026-10-25
4
+
5
+ - 新增 `/db` 斜杠指令:输入 `/` 后可从指令列表直接打开数据库面板,带数据库图标
6
+ - 修复对话内数据库 tab 中数据表列表与数据页滚动未分离的问题;现在左侧表列表和右侧数据各自独立滚动
7
+ - 新增对话数据库面板「查询历史」tab:按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时、行数和成功状态,支持一键复现到 SQL 编辑器
8
+ - 左侧全局面板新增「查询历史」tab:按对话分组展示历史,点击分组或记录可进入对应对话
9
+
3
10
  ## v1.0.1 — 2026-10-24
4
11
 
5
12
  首个稳定版。
package/README.en.md CHANGED
@@ -5,6 +5,7 @@ English | [简体中文](./README.md)
5
5
  [![test](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml/badge.svg)](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml)
6
6
  [![License](https://img.shields.io/github/license/etShaw-zh/dsh-db-studio)](LICENSE)
7
7
  [![release](https://img.shields.io/github/v/release/etShaw-zh/dsh-db-studio)](https://github.com/etShaw-zh/dsh-db-studio/releases)
8
+ [![npm](https://img.shields.io/npm/v/dsh-db-studio)](https://www.npmjs.com/package/dsh-db-studio)
8
9
 
9
10
  Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH), then query and edit data right in the conversation.
10
11
 
@@ -22,12 +23,22 @@ Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepse
22
23
  - **Database tab in a conversation**: view and work with the selected databases without leaving the chat.
23
24
  - **Multi-select picker in the composer**: choose the databases this conversation may use.
24
25
  - **Talk to the data**: four agent tools for querying, creating tables and writing data.
26
+ - **`/db` command**: type `/`, pick **Database** from the command list, and open the database panel.
27
+ - **Query history**: a **Query history** tab in the conversation database panel groups SQL by user turn, supports collapsing, and records SQL, connection, timing and affected rows; click an entry to load it back into the SQL editor. The global database panel groups the same history by conversation, and clicking a group opens that conversation.
25
28
 
26
29
  ## Install
27
30
 
31
+ From npm:
32
+
28
33
  ```bash
29
34
  # macOS desktop
30
35
  DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
36
+ "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
37
+ ```
38
+
39
+ Or install directly from GitHub:
40
+
41
+ ```bash
31
42
  "$DSH_CLI" plugin --profile desktop add github:etShaw-zh/dsh-db-studio
32
43
  ```
33
44
 
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![test](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml/badge.svg)](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml)
6
6
  [![License](https://img.shields.io/github/license/etShaw-zh/dsh-db-studio)](LICENSE)
7
7
  [![release](https://img.shields.io/github/v/release/etShaw-zh/dsh-db-studio)](https://github.com/etShaw-zh/dsh-db-studio/releases)
8
+ [![npm](https://img.shields.io/npm/v/dsh-db-studio)](https://www.npmjs.com/package/dsh-db-studio)
8
9
 
9
10
  在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)里连接数据库,直接在对话中查数据、改数据。
10
11
 
@@ -20,12 +21,22 @@
20
21
  - **对话内「数据库」标签**:在当前对话里直接查看和操作已选数据库。
21
22
  - **输入框旁多选下拉**:选择本次对话要使用的数据库,支持多选。
22
23
  - **对话中直接操作**:内置 4 个助手工具,可以帮你查询、建表、写数据。
24
+ - **`/db` 指令**:输入 `/`,在指令列表里选择「数据库」,直接打开数据库面板。
25
+ - **查询历史**:对话数据库面板新增「查询历史」tab,按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时和影响行数,点击即可复现;左侧全局面板的查询历史按对话分组,点击可进入对应对话。
23
26
 
24
27
  ## 安装
25
28
 
29
+ 从 npm 安装:
30
+
26
31
  ```bash
27
32
  # macOS 桌面端
28
33
  DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
34
+ "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
35
+ ```
36
+
37
+ 也可以直接从 GitHub 安装:
38
+
39
+ ```bash
29
40
  "$DSH_CLI" plugin --profile desktop add github:etShaw-zh/dsh-db-studio
30
41
  ```
31
42
 
package/lib/client.js CHANGED
@@ -26,6 +26,24 @@ window.__ModuleLoader__.load({
26
26
  schema: "结构",
27
27
  data: "数据",
28
28
  sql: "SQL",
29
+ history: "查询历史",
30
+ historyEmpty: "还没有查询记录。在 SQL 页运行查询后会出现在这里。",
31
+ historyClear: "清空历史",
32
+ historyReproduce: "复现",
33
+ historyReproduceHint: "载入到 SQL 页,不会自动执行",
34
+ historySuccess: "成功",
35
+ historyFailed: "失败",
36
+ historyRows: "{n} 行",
37
+ historyChanges: "影响 {n} 行",
38
+ historyAgent: "助手",
39
+ historyConsole: "控制台",
40
+ historyUnavailable: "连接 {name} 已不在当前会话的选择范围内",
41
+ historyOpen: "进入对话",
42
+ historyOpenFailed: "无法打开对话",
43
+ historyOpenFailedDetail: "打开对话失败:{error}",
44
+ historyTurn: "第 {n} 轮",
45
+ historyManual: "手动查询",
46
+ historyTurnNoPrompt: "未命名输入",
29
47
  run: "运行",
30
48
  running: "运行中…",
31
49
  searchTable: "搜索表…",
@@ -126,7 +144,9 @@ window.__ModuleLoader__.load({
126
144
  connectedOf: "{ok}/{n} 已连接",
127
145
  allConnected: "已连接",
128
146
  schemaHint: "点击「结构」查看字段",
129
- askInChat: "可以直接在对话里让助手查询或修改这些数据"
147
+ askInChat: "可以直接在对话里让助手查询或修改这些数据",
148
+ commandLabel: "数据库",
149
+ commandDesc: "打开数据库面板"
130
150
  };
131
151
  var en = {
132
152
  title: "Database",
@@ -143,6 +163,24 @@ window.__ModuleLoader__.load({
143
163
  schema: "Schema",
144
164
  data: "Data",
145
165
  sql: "SQL",
166
+ history: "Query history",
167
+ historyEmpty: "No queries yet. Run one from the SQL tab and it will show up here.",
168
+ historyClear: "Clear history",
169
+ historyReproduce: "Reproduce",
170
+ historyReproduceHint: "Load into the SQL tab without running it",
171
+ historySuccess: "OK",
172
+ historyFailed: "Failed",
173
+ historyRows: "{n} rows",
174
+ historyChanges: "{n} rows affected",
175
+ historyAgent: "Assistant",
176
+ historyConsole: "Console",
177
+ historyUnavailable: "Connection {name} is no longer selected for this conversation",
178
+ historyOpen: "Open conversation",
179
+ historyOpenFailed: "Could not open the conversation",
180
+ historyOpenFailedDetail: "Could not open the conversation: {error}",
181
+ historyTurn: "Turn {n}",
182
+ historyManual: "Manual queries",
183
+ historyTurnNoPrompt: "Untitled input",
146
184
  run: "Run",
147
185
  running: "Running…",
148
186
  searchTable: "Search tables…",
@@ -243,7 +281,9 @@ window.__ModuleLoader__.load({
243
281
  connectedOf: "{ok}/{n} connected",
244
282
  allConnected: "Connected",
245
283
  schemaHint: "Use Schema to inspect columns",
246
- askInChat: "Ask the assistant to query or change this data in chat"
284
+ askInChat: "Ask the assistant to query or change this data in chat",
285
+ commandLabel: "Database",
286
+ commandDesc: "Open the database panel"
247
287
  };
248
288
 
249
289
  function interpolate(text, params) {
@@ -514,6 +554,7 @@ window.__ModuleLoader__.load({
514
554
  function CloseIcon(props) { return h(Svg, props, h("path", { d: "M6 6l12 12M18 6 6 18" })); }
515
555
  function CheckIcon(props) { return h(Svg, props, h("path", { d: "M5 12.5 10 17.5 19 7" })); }
516
556
  function SearchIcon(props) { return h(Svg, props, h("circle", { cx: 11, cy: 11, r: 6.5 }), h("path", { d: "m16 16 4 4" })); }
557
+ function HistoryIcon(props) { return h(Svg, props, h("circle", { cx: 12, cy: 12, r: 8 }), h("path", { d: "M12 7.5V12l3 2" })); }
517
558
  function SparkIcon(props) { return h(Svg, props, h("path", { d: "M12 3.5 13.8 9l5.5 1.8-5.5 1.8L12 18l-1.8-5.4L4.7 10.8 10.2 9 12 3.5Z" })); }
518
559
  function ChevronDownIcon(props) { return h(Svg, props, h("path", { d: "m6 9.5 6 6 6-6" })); }
519
560
  function CopyIcon(props) { return h(Svg, props, h("rect", { x: 8.5, y: 8.5, width: 11, height: 11, rx: 2 }), h("path", { d: "M15.5 5.5h-9a2 2 0 0 0-2 2v9" })); }
@@ -710,7 +751,12 @@ window.__ModuleLoader__.load({
710
751
  .dshdb-statuspop-name{font-size:12px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
711
752
  .dshdb-statuspop-detail{font-size:10.5px;color:var(--dsw-alias-label-tertiary);line-height:1.4;word-break:break-word}
712
753
  .dshdb-statuspop-row:has(.dshdb-dot.bad) .dshdb-statuspop-detail{color:var(--dsw-alias-state-error-primary)}
713
- .dshdb-cv{height:100%;min-height:420px;display:flex;flex-direction:column;padding:14px 22px 16px;box-sizing:border-box;overflow:hidden;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);background:var(--dsw-alias-bg-base)}
754
+ .dshdb-cv{height:100%;min-height:0;display:flex;flex-direction:column;padding:14px 22px 16px;box-sizing:border-box;overflow:hidden;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);background:var(--dsw-alias-bg-base)}
755
+ /* The conversation shell gives a view an auto-height flex row; without bounding
756
+ that ancestor the workbench grows with the table list and drags the data pane
757
+ with it. Bounding it keeps the table list and the data grid on separate
758
+ scroll ports, exactly like the global panel. */
759
+ *:has(> * > .dshdb-cv){flex:1 1 0!important;min-height:0!important;overflow:hidden!important}
714
760
  .dshdb-cv *{box-sizing:border-box}
715
761
  .dshdb-cvhead{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;flex:0 0 auto}
716
762
  .dshdb-cvtitlewrap{display:flex;align-items:center;gap:10px;min-width:0}
@@ -731,6 +777,38 @@ window.__ModuleLoader__.load({
731
777
  .dshdb-cv .dshdb-pager{padding:7px 12px}
732
778
  .dshdb-cv .dshdb-sqlpane{padding:12px 14px;gap:10px;height:100%}
733
779
  .dshdb-cv .dshdb-tabs{margin-top:3px;flex:0 0 auto}
780
+ .dshdb-historypage{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:14px 18px;gap:8px;overflow:hidden}
781
+ .dshdb-historypagehead{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}
782
+ .dshdb-historygroups{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding:4px 2px 6px}
783
+ .dshdb-historygroup{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;overflow:hidden;background:var(--dsw-alias-bg-base)}
784
+ .dshdb-historygrouphead{appearance:none;border:0;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 38%,var(--dsw-alias-bg-base));color:var(--dsw-alias-label-primary);font:inherit;width:100%;text-align:left;display:flex;align-items:center;gap:9px;padding:9px 11px;cursor:pointer}
785
+ .dshdb-historygrouphead:hover{background:var(--dsw-alias-interactive-bg-hover)}
786
+ .dshdb-historygroupmain{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:2px}
787
+ .dshdb-historygrouptitle{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
788
+ .dshdb-historygroupcwd{font-size:10px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
789
+ .dshdb-historygroupcount{font-size:9.5px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;padding:1px 6px;flex:0 0 auto}
790
+ .dshdb-historyopen{display:inline-flex;align-items:center;gap:3px;font-size:10.5px;color:var(--dsw-alias-brand-primary);flex:0 0 auto}
791
+ .dshdb-historygrouprows{display:flex;flex-direction:column;gap:4px;padding:6px}
792
+ .dshdb-historypane{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:12px 14px;gap:8px;overflow:hidden}
793
+ .dshdb-historybar{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:2px 2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}
794
+ .dshdb-historytitle{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600}
795
+ .dshdb-historyturn{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-base)}
796
+ .dshdb-historyturnhead{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;width:100%;text-align:left;display:flex;align-items:center;gap:7px;padding:7px 9px;cursor:pointer}
797
+ .dshdb-historyturnhead:hover{background:var(--dsw-alias-interactive-bg-hover)}
798
+ .dshdb-historyturnlabel{font-size:11px;font-weight:600;flex:0 0 auto;white-space:nowrap}
799
+ .dshdb-historyturnprompt{font-size:11px;color:var(--dsw-alias-label-tertiary);min-width:0;flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
800
+ .dshdb-historyturncount{font-size:9.5px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;padding:1px 6px;flex:0 0 auto}
801
+ .dshdb-historyturnrows{display:flex;flex-direction:column;gap:4px;padding:6px;border-top:1px solid var(--dsw-alias-border-l2)}
802
+ .dshdb-historylist{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px 2px}
803
+ .dshdb-historyrow{display:flex;align-items:flex-start;gap:8px;width:100%;text-align:left;appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:8px;padding:8px 9px;cursor:pointer;font:inherit;color:var(--dsw-alias-label-primary)}
804
+ .dshdb-historyrow:hover{background:var(--dsw-alias-interactive-bg-hover)}
805
+ .dshdb-historyrow.bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 28%,var(--dsw-alias-border-l2))}
806
+ .dshdb-historystatus{display:flex;color:var(--dsw-alias-state-success-primary);flex:0 0 auto;margin-top:2px}
807
+ .dshdb-historyrow.bad .dshdb-historystatus{color:var(--dsw-alias-state-error-primary)}
808
+ .dshdb-historymain{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:3px}
809
+ .dshdb-historysql{font-family:var(--ds-font-family-code);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary)}
810
+ .dshdb-historymeta{display:flex;gap:7px;flex-wrap:wrap;font-size:10px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
811
+ .dshdb-historyaction{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;font-size:10.5px;color:var(--dsw-alias-brand-primary);padding:2px 3px;white-space:nowrap}
734
812
  .dshdb-cvempty{flex:1;display:flex;align-items:center;justify-content:center;padding:26px;min-height:300px}
735
813
  .dshdb-cvloading{flex:1;display:flex;align-items:center;justify-content:center;min-height:300px}
736
814
  .dshdb-cvmessage{padding:6px 0;flex:0 0 auto}
@@ -1075,6 +1153,10 @@ window.__ModuleLoader__.load({
1075
1153
  var [sqlError, setSqlError] = React.useState(null);
1076
1154
  var [sqlLoading, setSqlLoading] = React.useState(false);
1077
1155
  var [sqlHistory, setSqlHistory] = React.useState([]);
1156
+ var [historyGroups, setHistoryGroups] = React.useState([]);
1157
+ var [historyLoading, setHistoryLoading] = React.useState(false);
1158
+ var [historyError, setHistoryError] = React.useState(null);
1159
+ var [sessionTitles, setSessionTitles] = React.useState({});
1078
1160
  var [dialog, setDialog] = React.useState(null);
1079
1161
  var [pendingDelete, setPendingDelete] = React.useState(null);
1080
1162
  var [busy, setBusy] = React.useState(false);
@@ -1097,6 +1179,52 @@ window.__ModuleLoader__.load({
1097
1179
  return function () { alive = false; };
1098
1180
  }, []);
1099
1181
 
1182
+ React.useEffect(function () {
1183
+ var sessions = APP_CTX && APP_CTX.get ? APP_CTX.get("sessions") : null;
1184
+ if (!sessions || !sessions.list) return;
1185
+ function update() {
1186
+ var snapshot = sessions.list.getSnapshot();
1187
+ setSessionTitles(snapshot && snapshot.byId ? snapshot.byId : {});
1188
+ }
1189
+ update();
1190
+ var off = sessions.list.subscribe ? sessions.list.subscribe(update) : null;
1191
+ return function () { if (typeof off === "function") off(); };
1192
+ }, []);
1193
+
1194
+ React.useEffect(function () {
1195
+ if (tab === "history") loadAllHistory();
1196
+ }, [tab]);
1197
+
1198
+ function loadAllHistory() {
1199
+ setHistoryLoading(true);
1200
+ setHistoryError(null);
1201
+ api({ action: "history-all" }).then(function (data) {
1202
+ setHistoryGroups(Array.isArray(data.sessions) ? data.sessions : []);
1203
+ }).catch(function (failure) {
1204
+ setHistoryError(interpolate(t("loadFailed"), { error: failure.message }));
1205
+ }).finally(function () { setHistoryLoading(false); });
1206
+ }
1207
+
1208
+ function sessionLabel(sessionId) {
1209
+ var row = sessionTitles[sessionId];
1210
+ var title = row && (row.title || row.displayTitle);
1211
+ if (title && String(title).trim()) return String(title).trim();
1212
+ return String(sessionId || "").slice(0, 8);
1213
+ }
1214
+
1215
+ function openConversation(sessionId) {
1216
+ var workspace = APP_CTX && APP_CTX.get ? APP_CTX.get("uiWorkspace") : null;
1217
+ if (!workspace || typeof workspace.openSession !== "function") {
1218
+ setHistoryError(t("historyOpenFailed"));
1219
+ return;
1220
+ }
1221
+ try {
1222
+ workspace.openSession(sessionId);
1223
+ } catch (failure) {
1224
+ setHistoryError(interpolate(t("historyOpenFailedDetail"), { error: failure.message }));
1225
+ }
1226
+ }
1227
+
1100
1228
  React.useEffect(function () {
1101
1229
  if (!activeId) { setTables([]); setSelectedTable(null); setSchema(null); setResult(null); return; }
1102
1230
  var alive = true;
@@ -1441,6 +1569,62 @@ window.__ModuleLoader__.load({
1441
1569
  ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
1442
1570
  );
1443
1571
 
1572
+ var historyPane = h("div", { className: "dshdb-historypage" },
1573
+ h("div", { className: "dshdb-historypagehead" },
1574
+ h("div", { className: "dshdb-historytitle" },
1575
+ h(HistoryIcon, { size: 14 }),
1576
+ t("history"),
1577
+ h("span", { className: "dshdb-gridmeta" },
1578
+ historyGroups.reduce(function (sum, group) { return sum + (group.entries ? group.entries.length : 0); }, 0))
1579
+ ),
1580
+ h("button", { type: "button", className: "dshdb-btn small ghost", disabled: historyLoading, onClick: loadAllHistory }, h(RefreshIcon, { size: 13 }), t("refresh"))
1581
+ ),
1582
+ historyError ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, historyError)) : null,
1583
+ historyLoading
1584
+ ? h("div", { className: "dshdb-placeholder" }, h(Spinner))
1585
+ : historyGroups.length === 0
1586
+ ? h("div", { className: "dshdb-placeholder" }, t("historyEmpty"))
1587
+ : h("div", { className: "dshdb-historygroups" }, historyGroups.map(function (group) {
1588
+ var rows = Array.isArray(group.entries) ? group.entries : [];
1589
+ var folder = group.cwd ? String(group.cwd).split(/[\\/]/).filter(Boolean).pop() : "";
1590
+ return h("div", { key: group.sessionId, className: "dshdb-historygroup" },
1591
+ h("button", { type: "button", className: "dshdb-historygrouphead", onClick: function () { openConversation(group.sessionId); } },
1592
+ h("span", { className: "dshdb-historygroupmain" },
1593
+ h("span", { className: "dshdb-historygrouptitle" }, sessionLabel(group.sessionId)),
1594
+ folder ? h("span", { className: "dshdb-historygroupcwd" }, folder) : null
1595
+ ),
1596
+ h("span", { className: "dshdb-historygroupcount" }, rows.length),
1597
+ h("span", { className: "dshdb-historyopen" }, t("historyOpen"), h(ChevronRightIcon, { size: 11 }))
1598
+ ),
1599
+ h("div", { className: "dshdb-historygrouprows" }, rows.map(function (row) {
1600
+ var ok = row.ok !== false;
1601
+ var source = row.source === "agent" ? t("historyAgent") : t("historyConsole");
1602
+ var count = row.changes != null
1603
+ ? interpolate(t("historyChanges"), { n: fmtNumber(row.changes) })
1604
+ : interpolate(t("historyRows"), { n: fmtNumber(row.rows || 0) });
1605
+ return h("button", {
1606
+ key: row.id,
1607
+ type: "button",
1608
+ className: "dshdb-historyrow" + (ok ? "" : " bad"),
1609
+ title: (ok ? "" : (row.error || t("historyFailed")) + " · ") + t("historyOpen"),
1610
+ onClick: function () { openConversation(group.sessionId); }
1611
+ },
1612
+ h("span", { className: "dshdb-historystatus" }, h(ok ? CheckIcon : CloseIcon, { size: 13 })),
1613
+ h("span", { className: "dshdb-historymain" },
1614
+ h("span", { className: "dshdb-historysql" }, row.sql),
1615
+ h("span", { className: "dshdb-historymeta" },
1616
+ h("span", null, row.connectionName || row.connectionId),
1617
+ h("span", null, source),
1618
+ h("span", null, new Date(row.at).toLocaleString()),
1619
+ h("span", null, count)
1620
+ )
1621
+ )
1622
+ );
1623
+ }))
1624
+ );
1625
+ }))
1626
+ );
1627
+
1444
1628
  main = h("section", { className: "dshdb-main" },
1445
1629
  h("div", { className: "dshdb-mainhead" },
1446
1630
  h("div", { className: "dshdb-mainheadtop" },
@@ -1468,11 +1652,12 @@ window.__ModuleLoader__.load({
1468
1652
  ),
1469
1653
  h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
1470
1654
  h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
1471
- h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
1655
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql")),
1656
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "history", className: "dshdb-tab" + (tab === "history" ? " active" : ""), onClick: function () { setTab("history"); } }, h(HistoryIcon, { size: 14 }), t("history"))
1472
1657
  )
1473
1658
  ),
1474
- tab === "data" ? dataPane : sqlPane,
1475
- h("div", { className: "dshdb-chatbar" },
1659
+ tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane,
1660
+ tab === "history" ? null : h("div", { className: "dshdb-chatbar" },
1476
1661
  h(SparkIcon, { size: 14 }),
1477
1662
  h("div", null, h("b", null, t("inChat") + " · "), t("inChatHint"))
1478
1663
  )
@@ -1840,6 +2025,47 @@ window.__ModuleLoader__.load({
1840
2025
  }
1841
2026
 
1842
2027
  // ── conversation tab: the selected databases ────────────────────────
2028
+ function messageContentText(content) {
2029
+ if (typeof content === "string") return content;
2030
+ if (!Array.isArray(content)) return "";
2031
+ return content.map(function (block) {
2032
+ return block && block.type === "text" && typeof block.text === "string" ? block.text : "";
2033
+ }).join(" ").replace(/\s+/g, " ").trim();
2034
+ }
2035
+
2036
+ /** Group recorded SQL by the loaded conversation turn whose time window contains it. */
2037
+ function sqlHistoryTurns(history, turns, nodes, t) {
2038
+ var loaded = (Array.isArray(turns) ? turns : []).map(function (item) {
2039
+ var node = nodes && typeof nodes.get === "function" ? nodes.get(item.anchorKey) : null;
2040
+ return {
2041
+ id: "turn-" + item.turn,
2042
+ turn: Number(item.turn) || 0,
2043
+ time: Number(node && node.data ? node.data.time : 0) || 0,
2044
+ prompt: item.prompt || "",
2045
+ entries: [],
2046
+ };
2047
+ });
2048
+ if (loaded.length === 0) return [{ id: "manual", index: 0, label: t("historyManual"), prompt: "", entries: history }];
2049
+ var groups = loaded.map(function (turn) {
2050
+ return { id: turn.id, index: 0, label: interpolate(t("historyTurn"), { n: turn.turn }), prompt: turn.prompt, time: turn.time, entries: [] };
2051
+ });
2052
+ var orphans = [];
2053
+ history.forEach(function (row) {
2054
+ var at = Number(row.at) || 0;
2055
+ var index = -1;
2056
+ for (var cursor = 0; cursor < loaded.length; cursor += 1) {
2057
+ if (loaded[cursor].time <= at) index = cursor;
2058
+ else break;
2059
+ }
2060
+ if (index < 0) orphans.push(row);
2061
+ else groups[index].entries.push(row);
2062
+ });
2063
+ var result = groups.filter(function (group) { return group.entries.length > 0; }).reverse();
2064
+ if (orphans.length) result.push({ id: "orphans", index: result.length, label: t("historyManual"), prompt: "", entries: orphans });
2065
+ result.forEach(function (group, index) { group.index = index; });
2066
+ return result;
2067
+ }
2068
+
1843
2069
  function ConversationDbView(props) {
1844
2070
  var t = props.t || function (key) { return key; };
1845
2071
  var sessionId = props.sessionId;
@@ -1866,6 +2092,15 @@ window.__ModuleLoader__.load({
1866
2092
  var [sqlResult, setSqlResult] = React.useState(null);
1867
2093
  var [sqlError, setSqlError] = React.useState(null);
1868
2094
  var [sqlLoading, setSqlLoading] = React.useState(false);
2095
+ var [history, setHistory] = React.useState([]);
2096
+ var [historyLoading, setHistoryLoading] = React.useState(false);
2097
+ var [historyError, setHistoryError] = React.useState(null);
2098
+ var [collapsedTurns, setCollapsedTurns] = React.useState({});
2099
+ var fallbackUseChat = function () { return undefined; };
2100
+ var useChat = typeof props.useChat === "function" ? props.useChat : fallbackUseChat;
2101
+ var chatTurns = useChat(function (snapshot) { return snapshot.navigation ? snapshot.navigation.items() : undefined; });
2102
+ var chatNodes = useChat(function (snapshot) { return snapshot.nodes; });
2103
+ var turnGroups = sqlHistoryTurns(history, chatTurns, chatNodes, t);
1869
2104
  var [dialog, setDialog] = React.useState(null);
1870
2105
  var [busy, setBusy] = React.useState(false);
1871
2106
  var [error, setError] = React.useState(null);
@@ -1906,6 +2141,44 @@ window.__ModuleLoader__.load({
1906
2141
  return function () { alive = false; };
1907
2142
  }, [activeId, sessionId]);
1908
2143
 
2144
+ React.useEffect(function () {
2145
+ if (tab === "history") loadHistory();
2146
+ }, [tab, sessionId]);
2147
+
2148
+ function loadHistory() {
2149
+ if (!sessionId) return;
2150
+ setHistoryLoading(true);
2151
+ setHistoryError(null);
2152
+ api({ action: "history-list", sessionId: sessionId }).then(function (data) {
2153
+ setHistory(Array.isArray(data.entries) ? data.entries : []);
2154
+ }).catch(function (failure) {
2155
+ setHistoryError(interpolate(t("loadFailed"), { error: failure.message }));
2156
+ }).finally(function () { setHistoryLoading(false); });
2157
+ }
2158
+
2159
+ function clearHistory() {
2160
+ if (!sessionId) return;
2161
+ api({ action: "history-clear", sessionId: sessionId }).then(function () {
2162
+ setHistory([]);
2163
+ }).catch(function (failure) {
2164
+ setHistoryError(interpolate(t("saveFailed"), { error: failure.message }));
2165
+ });
2166
+ }
2167
+
2168
+ function reproduceHistory(row) {
2169
+ var connection = selected.find(function (item) { return item.id === row.connectionId; });
2170
+ if (!connection) {
2171
+ setError(interpolate(t("historyUnavailable"), { name: row.connectionName || row.connectionId }));
2172
+ return;
2173
+ }
2174
+ setError(null);
2175
+ setActiveId(row.connectionId);
2176
+ setSql(row.sql || "");
2177
+ setSqlResult(null);
2178
+ setSqlError(null);
2179
+ setTab("sql");
2180
+ }
2181
+
1909
2182
  function selectTable(connectionId, name, nextPage, internal) {
1910
2183
  setSelectedTable(name);
1911
2184
  setPage(nextPage || 0);
@@ -1968,7 +2241,10 @@ window.__ModuleLoader__.load({
1968
2241
  setSqlResult({ readOnly: readOnly, data: data });
1969
2242
  }).catch(function (failure) {
1970
2243
  setSqlError(interpolate(t("queryFailed"), { error: failure.message }));
1971
- }).finally(function () { setSqlLoading(false); });
2244
+ }).finally(function () {
2245
+ setSqlLoading(false);
2246
+ loadHistory();
2247
+ });
1972
2248
  }
1973
2249
 
1974
2250
  async function openDemo() {
@@ -2176,6 +2452,75 @@ window.__ModuleLoader__.load({
2176
2452
  ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
2177
2453
  );
2178
2454
 
2455
+ var historyPane = h("div", { className: "dshdb-historypane" },
2456
+ h("div", { className: "dshdb-historybar" },
2457
+ h("div", { className: "dshdb-historytitle" },
2458
+ h(HistoryIcon, { size: 14 }),
2459
+ t("history"),
2460
+ h("span", { className: "dshdb-gridmeta" }, history.length)
2461
+ ),
2462
+ h("button", {
2463
+ type: "button",
2464
+ className: "dshdb-btn small ghost",
2465
+ disabled: historyLoading || history.length === 0,
2466
+ onClick: clearHistory
2467
+ }, h(TrashIcon, { size: 13 }), t("historyClear"))
2468
+ ),
2469
+ historyError ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, historyError)) : null,
2470
+ historyLoading
2471
+ ? h("div", { className: "dshdb-placeholder" }, h(Spinner))
2472
+ : history.length === 0
2473
+ ? h("div", { className: "dshdb-placeholder" }, t("historyEmpty"))
2474
+ : h("div", { className: "dshdb-historylist" }, turnGroups.map(function (group) {
2475
+ var collapsed = collapsedTurns[group.id] !== undefined ? collapsedTurns[group.id] : group.index > 0;
2476
+ return h("div", { key: group.id, className: "dshdb-historyturn" },
2477
+ h("button", {
2478
+ type: "button",
2479
+ className: "dshdb-historyturnhead",
2480
+ "aria-expanded": !collapsed,
2481
+ onClick: function () {
2482
+ setCollapsedTurns(function (previous) {
2483
+ var next = Object.assign({}, previous);
2484
+ next[group.id] = !collapsed;
2485
+ return next;
2486
+ });
2487
+ }
2488
+ },
2489
+ h(ChevronDownIcon, { size: 13, style: { transform: collapsed ? "rotate(-90deg)" : "none", transition: "transform .12s ease" } }),
2490
+ h("span", { className: "dshdb-historyturnlabel" }, group.label),
2491
+ group.prompt ? h("span", { className: "dshdb-historyturnprompt", title: group.prompt }, group.prompt) : null,
2492
+ h("span", { className: "dshdb-historyturncount" }, group.entries.length)
2493
+ ),
2494
+ collapsed ? null : h("div", { className: "dshdb-historyturnrows" }, group.entries.map(function (row) {
2495
+ var ok = row.ok !== false;
2496
+ var source = row.source === "agent" ? t("historyAgent") : t("historyConsole");
2497
+ var count = row.changes != null
2498
+ ? interpolate(t("historyChanges"), { n: fmtNumber(row.changes) })
2499
+ : interpolate(t("historyRows"), { n: fmtNumber(row.rows || 0) });
2500
+ return h("button", {
2501
+ key: row.id,
2502
+ type: "button",
2503
+ className: "dshdb-historyrow" + (ok ? "" : " bad"),
2504
+ title: ok ? t("historyReproduceHint") : (row.error || t("historyFailed")),
2505
+ onClick: function () { reproduceHistory(row); }
2506
+ },
2507
+ h("span", { className: "dshdb-historystatus" }, h(ok ? CheckIcon : CloseIcon, { size: 13 })),
2508
+ h("span", { className: "dshdb-historymain" },
2509
+ h("span", { className: "dshdb-historysql" }, row.sql),
2510
+ h("span", { className: "dshdb-historymeta" },
2511
+ h("span", null, row.connectionName || row.connectionId),
2512
+ h("span", null, source),
2513
+ h("span", null, new Date(row.at).toLocaleString()),
2514
+ h("span", null, count)
2515
+ )
2516
+ ),
2517
+ h("span", { className: "dshdb-historyaction" }, h(PlayIcon, { size: 11 }), t("historyReproduce"))
2518
+ );
2519
+ }))
2520
+ );
2521
+ }))
2522
+ );
2523
+
2179
2524
  return h("div", { className: "dshdb-cv" },
2180
2525
  h("div", { className: "dshdb-cvhead" },
2181
2526
  h("div", { className: "dshdb-cvtitlewrap" },
@@ -2208,10 +2553,11 @@ window.__ModuleLoader__.load({
2208
2553
  ),
2209
2554
  h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
2210
2555
  h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
2211
- h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
2556
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql")),
2557
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "history", className: "dshdb-tab" + (tab === "history" ? " active" : ""), onClick: function () { setTab("history"); } }, h(HistoryIcon, { size: 14 }), t("history"))
2212
2558
  ),
2213
2559
  error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
2214
- h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : sqlPane),
2560
+ h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane),
2215
2561
  dialogNode
2216
2562
  );
2217
2563
  }
@@ -2221,8 +2567,13 @@ window.__ModuleLoader__.load({
2221
2567
  return h(DatabaseIcon, { size: props.size || 18 });
2222
2568
  }
2223
2569
 
2570
+ /** Slash-command glyph (the trigger menu asks for a 14px icon). */
2571
+ function DbCommandIcon(props) {
2572
+ return h(DatabaseIcon, { size: props.size || 14 });
2573
+ }
2574
+
2224
2575
  exports.name = "dsh-db-studio";
2225
- exports.inject = ["slots", "layout", "locale"];
2576
+ exports.inject = ["slots", "layout", "locale", "commandUi"];
2226
2577
  exports.apply = function (ctx) {
2227
2578
  APP_CTX = ctx;
2228
2579
  installStyles(ctx);
@@ -2231,6 +2582,25 @@ window.__ModuleLoader__.load({
2231
2582
  }, "dsh-db-studio: dictionaries");
2232
2583
  var t = ctx.locale.bind(NS);
2233
2584
 
2585
+ // `/db` in the composer: same row treatment as the built-in commands,
2586
+ // with a database glyph in front and no message submitted.
2587
+ var commandUi = ctx.get("commandUi");
2588
+ if (commandUi && typeof commandUi.register === "function") {
2589
+ ctx.effect(function () {
2590
+ return commandUi.register({
2591
+ name: "db",
2592
+ available: function () { return true; },
2593
+ label: function () { return t("commandLabel"); },
2594
+ description: function () { return t("commandDesc"); },
2595
+ icon: DbCommandIcon,
2596
+ ui: {
2597
+ kind: "action",
2598
+ run: function () { openGlobalPanel(); }
2599
+ }
2600
+ });
2601
+ }, "dsh-db-studio: /db command");
2602
+ }
2603
+
2234
2604
  ctx.slots.inject("main", function () {
2235
2605
  return ctx.slots.register({
2236
2606
  name: "main",
package/lib/index.js CHANGED
@@ -284,7 +284,7 @@ function buildToolDefinitions(studio) {
284
284
  async execute(args, exec) {
285
285
  const session = sessionOf(exec)
286
286
  const id = await resolveConnectionId(studio, args?.connectionId, session)
287
- const result = await studio.query(id, args?.sql, { limit: args?.limit, ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
287
+ const result = await studio.query(id, args?.sql, { limit: args?.limit, source: 'agent', ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
288
288
  const rows = result.rows ?? []
289
289
  const summaryParts = [`查询成功,返回 ${rows.length} 行${result.truncated ? '(已截断)' : ''},耗时 ${result.elapsedMs} ms。`]
290
290
  const table = markdownTable(result.columns, rows, 25)
@@ -328,7 +328,7 @@ function buildToolDefinitions(studio) {
328
328
  if (args?.confirm !== true) throw new Error('写操作需要显式确认:请先在对话中得到用户同意,再以 confirm: true 重试。')
329
329
  const session = sessionOf(exec)
330
330
  const id = await resolveConnectionId(studio, args?.connectionId, session)
331
- const result = await studio.execute(id, args?.sql, session ? { sessionId: session.id, cwd: session.cwd } : {})
331
+ const result = await studio.execute(id, args?.sql, { source: 'agent', ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
332
332
  const affected = result.changes ?? 0
333
333
  return {
334
334
  summary: `执行成功:影响 ${affected} 行${result.lastInsertRowid != null ? `,最后插入 id = ${result.lastInsertRowid}` : ''},耗时 ${result.elapsedMs ?? 0} ms。`,
@@ -365,9 +365,15 @@ async function dispatch(studio, body, options = {}) {
365
365
  case 'rows':
366
366
  return { ok: true, ...(await studio.rows(body.id, body.table, { limit: body.limit, offset: body.offset, ...scoped })) }
367
367
  case 'query':
368
- return { ok: true, ...(await studio.query(body.id, body.sql, { limit: body.limit, ...scoped })) }
368
+ return { ok: true, ...(await studio.query(body.id, body.sql, { limit: body.limit, source: 'ui', ...scoped })) }
369
369
  case 'execute':
370
- return { ok: true, ...(await studio.execute(body.id, body.sql, scoped)) }
370
+ return { ok: true, ...(await studio.execute(body.id, body.sql, { source: 'ui', ...scoped })) }
371
+ case 'history-list':
372
+ return { ok: true, ...(await studio.sqlHistory(sessionId)) }
373
+ case 'history-all':
374
+ return { ok: true, ...(await studio.allSqlHistory()) }
375
+ case 'history-clear':
376
+ return { ok: true, ...(await studio.clearSqlHistory(sessionId)) }
371
377
  case 'test':
372
378
  return { ok: true, ...(await studio.testConnection(body.connection ?? body)) }
373
379
  case 'add':
package/lib/service.js CHANGED
@@ -398,16 +398,73 @@ export class DatabaseStudio {
398
398
  return { connection: publicConnection(connection), table: String(table), limit, offset, ...result }
399
399
  }
400
400
 
401
+ /**
402
+ * Record one SQL run for the conversation's history. History is a convenience
403
+ * surface: a write failure must never turn a successful query into an error,
404
+ * so recording errors are logged and swallowed.
405
+ */
406
+ async #recordSqlHistory(sessionId, connection, sql, source, outcome) {
407
+ if (!sessionId) return
408
+ try {
409
+ await this.selections.addHistory(sessionId, {
410
+ sql: String(sql ?? '').slice(0, 20000),
411
+ connectionId: connection.id,
412
+ connectionName: connection.name,
413
+ source: source === 'agent' ? 'agent' : 'ui',
414
+ ...outcome,
415
+ })
416
+ } catch (error) {
417
+ console.warn('dsh-db-studio: failed to record SQL history —', error?.message ?? error)
418
+ }
419
+ }
420
+
421
+ /** Every conversation's SQL history, grouped by Session. */
422
+ async allSqlHistory() {
423
+ await this.ready()
424
+ return { sessions: await this.selections.allHistory() }
425
+ }
426
+
427
+ /** One Session's SQL history, newest first. */
428
+ async sqlHistory(sessionId) {
429
+ await this.ready()
430
+ if (!sessionId) throw new Error('缺少会话 id / missing session id')
431
+ return { sessionId, entries: await this.selections.history(sessionId) }
432
+ }
433
+
434
+ /** Clear one Session's SQL history. */
435
+ async clearSqlHistory(sessionId) {
436
+ await this.ready()
437
+ if (!sessionId) throw new Error('缺少会话 id / missing session id')
438
+ const cleared = await this.selections.clearHistory(sessionId)
439
+ return { sessionId, cleared }
440
+ }
441
+
401
442
  /** Run a read-only query. */
402
443
  async query(id, sql, options = {}) {
403
444
  if (options.sessionId) await this.assertSessionConnection(options.sessionId, id, options.cwd)
404
445
  const connection = await this.connection(id)
405
446
  const limit = clampLimit(options.limit, this.config.maxRows, this.config.maxRows)
406
447
  const adapter = await this.#adapter(connection)
448
+ const source = options.source
407
449
  const started = Date.now()
408
- const result = await adapter.query(sql, { limit })
409
- this.touchUsage(connection.id, options.sessionId, options.cwd)
410
- return { connection: publicConnection(connection), elapsedMs: Date.now() - started, ...result }
450
+ try {
451
+ const result = await adapter.query(sql, { limit })
452
+ await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
453
+ ok: true,
454
+ elapsedMs: Date.now() - started,
455
+ rows: Array.isArray(result.rows) ? result.rows.length : 0,
456
+ truncated: result.truncated === true,
457
+ })
458
+ this.touchUsage(connection.id, options.sessionId, options.cwd)
459
+ return { connection: publicConnection(connection), elapsedMs: Date.now() - started, ...result }
460
+ } catch (error) {
461
+ await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
462
+ ok: false,
463
+ elapsedMs: Date.now() - started,
464
+ error: error?.message ?? String(error),
465
+ })
466
+ throw error
467
+ }
411
468
  }
412
469
 
413
470
  /** Run a write statement (or any single statement) after policy checks. */
@@ -419,10 +476,26 @@ export class DatabaseStudio {
419
476
  if (!isSingleStatement(sql)) throw new Error('只允许执行一条 SQL / only one statement is allowed')
420
477
  return this.#serialized(connection.id, async () => {
421
478
  const adapter = await this.#adapter(connection)
479
+ const source = options.source
422
480
  const started = Date.now()
423
- const result = await adapter.execute(sql)
424
- this.touchUsage(connection.id, options.sessionId, options.cwd)
425
- return { connection: publicConnection(connection), elapsedMs: result.elapsedMs ?? (Date.now() - started), ...result }
481
+ try {
482
+ const result = await adapter.execute(sql)
483
+ await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
484
+ ok: true,
485
+ elapsedMs: result.elapsedMs ?? (Date.now() - started),
486
+ changes: result.changes ?? 0,
487
+ lastInsertRowid: result.lastInsertRowid == null ? undefined : String(result.lastInsertRowid),
488
+ })
489
+ this.touchUsage(connection.id, options.sessionId, options.cwd)
490
+ return { connection: publicConnection(connection), elapsedMs: result.elapsedMs ?? (Date.now() - started), ...result }
491
+ } catch (error) {
492
+ await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
493
+ ok: false,
494
+ elapsedMs: Date.now() - started,
495
+ error: error?.message ?? String(error),
496
+ })
497
+ throw error
498
+ }
426
499
  })
427
500
  }
428
501
 
@@ -20,6 +20,7 @@
20
20
  * @module dsh-db-studio/session-store
21
21
  */
22
22
 
23
+ import { randomUUID } from 'node:crypto'
23
24
  import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
24
25
  import { join } from 'node:path'
25
26
  import { studioHome } from './home.js'
@@ -27,11 +28,12 @@ import { studioHome } from './home.js'
27
28
  const FILE_MODE = 0o600
28
29
  const DIR_MODE = 0o700
29
30
  const MAX_SESSIONS = 200
31
+ const MAX_HISTORY = 100
30
32
  const SAVE_DELAY_MS = 400
31
33
 
32
34
  /** Empty state document. */
33
35
  function emptyState() {
34
- return { version: 1, sessions: {}, usage: {} }
36
+ return { version: 2, sessions: {}, usage: {}, history: {} }
35
37
  }
36
38
 
37
39
  /** Keep only the newest N sessions. */
@@ -42,6 +44,14 @@ function pruneSessions(sessions) {
42
44
  return Object.fromEntries(rows.slice(0, MAX_SESSIONS))
43
45
  }
44
46
 
47
+ /** Keep only the newest N history rows per Session. */
48
+ function pruneHistory(history) {
49
+ const rows = Object.entries(history)
50
+ if (rows.length <= MAX_SESSIONS) return history
51
+ rows.sort((left, right) => (right[1]?.[0]?.at ?? 0) - (left[1]?.[0]?.at ?? 0))
52
+ return Object.fromEntries(rows.slice(0, MAX_SESSIONS))
53
+ }
54
+
45
55
  /** Normalize one stored entry. */
46
56
  function normalizeEntry(entry) {
47
57
  return {
@@ -71,9 +81,10 @@ export class SessionSelectionStore {
71
81
  const parsed = JSON.parse(raw)
72
82
  if (parsed && typeof parsed === 'object') {
73
83
  this.state = {
74
- version: 1,
84
+ version: 2,
75
85
  sessions: parsed.sessions && typeof parsed.sessions === 'object' ? parsed.sessions : {},
76
86
  usage: parsed.usage && typeof parsed.usage === 'object' ? parsed.usage : {},
87
+ history: parsed.history && typeof parsed.history === 'object' ? parsed.history : {},
77
88
  }
78
89
  }
79
90
  } catch (error) {
@@ -93,6 +104,7 @@ export class SessionSelectionStore {
93
104
  this.saveQueue = this.saveQueue.catch(() => {}).then(async () => {
94
105
  await mkdir(this.dir, { recursive: true, mode: DIR_MODE })
95
106
  this.state.sessions = pruneSessions(this.state.sessions)
107
+ this.state.history = pruneHistory(this.state.history)
96
108
  const body = JSON.stringify(this.state, null, 2) + '\n'
97
109
  const temp = `${this.file}.${process.pid}.${Date.now()}.tmp`
98
110
  await writeFile(temp, body, { mode: FILE_MODE })
@@ -207,6 +219,56 @@ export class SessionSelectionStore {
207
219
  return normalizeEntry(this.#entry(sessionId)).ids
208
220
  }
209
221
 
222
+ /** Append one SQL run to a Session's history (newest first, bounded). */
223
+ async addHistory(sessionId, entry) {
224
+ if (!sessionId) return null
225
+ await this.load()
226
+ const record = {
227
+ id: randomUUID(),
228
+ at: Date.now(),
229
+ ...entry,
230
+ }
231
+ const rows = Array.isArray(this.state.history[sessionId]) ? this.state.history[sessionId] : []
232
+ rows.unshift(record)
233
+ this.state.history[sessionId] = rows.slice(0, MAX_HISTORY)
234
+ await this.save()
235
+ return record
236
+ }
237
+
238
+ /** Every Session's SQL history, newest activity first. */
239
+ async allHistory() {
240
+ await this.load()
241
+ const groups = []
242
+ for (const [sessionId, entries] of Object.entries(this.state.history)) {
243
+ if (!Array.isArray(entries) || entries.length === 0) continue
244
+ const session = normalizeEntry(this.state.sessions[sessionId])
245
+ groups.push({
246
+ sessionId,
247
+ cwd: session.cwd ?? null,
248
+ sessionUpdatedAt: session.updatedAt,
249
+ entries,
250
+ })
251
+ }
252
+ groups.sort((left, right) => (right.entries[0]?.at ?? 0) - (left.entries[0]?.at ?? 0))
253
+ return groups
254
+ }
255
+
256
+ /** One Session's SQL history, newest first. */
257
+ async history(sessionId) {
258
+ await this.load()
259
+ const rows = this.state.history[sessionId]
260
+ return Array.isArray(rows) ? rows.slice() : []
261
+ }
262
+
263
+ /** Clear one Session's SQL history. */
264
+ async clearHistory(sessionId) {
265
+ await this.load()
266
+ if (!this.state.history[sessionId]) return false
267
+ delete this.state.history[sessionId]
268
+ await this.save()
269
+ return true
270
+ }
271
+
210
272
  /** All usage rows (tests/debug). */
211
273
  usageSnapshot() {
212
274
  return { ...this.state.usage }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-studio",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Connect to databases in DeepSeek Harness (DSH), then query and edit data right in the conversation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,8 @@
42
42
  "inject": [
43
43
  "@deepseek-ai/dsh-client-locale",
44
44
  "@deepseek-ai/dsh-client-ui-layout",
45
- "@deepseek-ai/dsh-client-ui-sidebar"
45
+ "@deepseek-ai/dsh-client-ui-sidebar",
46
+ "@deepseek-ai/dsh-client-ui-commands"
46
47
  ]
47
48
  }
48
49
  },