dsh-db-studio 1.0.2 → 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,8 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## v1.0.2 — 2026-10-25
3
+ ## v1.0.3 — 2026-10-25
4
4
 
5
5
  - 新增 `/db` 斜杠指令:输入 `/` 后可从指令列表直接打开数据库面板,带数据库图标
6
+ - 修复对话内数据库 tab 中数据表列表与数据页滚动未分离的问题;现在左侧表列表和右侧数据各自独立滚动
7
+ - 新增对话数据库面板「查询历史」tab:按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时、行数和成功状态,支持一键复现到 SQL 编辑器
8
+ - 左侧全局面板新增「查询历史」tab:按对话分组展示历史,点击分组或记录可进入对应对话
6
9
 
7
10
  ## v1.0.1 — 2026-10-24
8
11
 
package/README.en.md CHANGED
@@ -24,6 +24,7 @@ Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepse
24
24
  - **Multi-select picker in the composer**: choose the databases this conversation may use.
25
25
  - **Talk to the data**: four agent tools for querying, creating tables and writing data.
26
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.
27
28
 
28
29
  ## Install
29
30
 
@@ -32,7 +33,7 @@ From npm:
32
33
  ```bash
33
34
  # macOS desktop
34
35
  DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
35
- "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.2
36
+ "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
36
37
  ```
37
38
 
38
39
  Or install directly from GitHub:
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  - **输入框旁多选下拉**:选择本次对话要使用的数据库,支持多选。
23
23
  - **对话中直接操作**:内置 4 个助手工具,可以帮你查询、建表、写数据。
24
24
  - **`/db` 指令**:输入 `/`,在指令列表里选择「数据库」,直接打开数据库面板。
25
+ - **查询历史**:对话数据库面板新增「查询历史」tab,按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时和影响行数,点击即可复现;左侧全局面板的查询历史按对话分组,点击可进入对应对话。
25
26
 
26
27
  ## 安装
27
28
 
@@ -30,7 +31,7 @@
30
31
  ```bash
31
32
  # macOS 桌面端
32
33
  DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
33
- "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.2
34
+ "$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
34
35
  ```
35
36
 
36
37
  也可以直接从 GitHub 安装:
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: "搜索表…",
@@ -145,6 +163,24 @@ window.__ModuleLoader__.load({
145
163
  schema: "Schema",
146
164
  data: "Data",
147
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",
148
184
  run: "Run",
149
185
  running: "Running…",
150
186
  searchTable: "Search tables…",
@@ -518,6 +554,7 @@ window.__ModuleLoader__.load({
518
554
  function CloseIcon(props) { return h(Svg, props, h("path", { d: "M6 6l12 12M18 6 6 18" })); }
519
555
  function CheckIcon(props) { return h(Svg, props, h("path", { d: "M5 12.5 10 17.5 19 7" })); }
520
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" })); }
521
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" })); }
522
559
  function ChevronDownIcon(props) { return h(Svg, props, h("path", { d: "m6 9.5 6 6 6-6" })); }
523
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" })); }
@@ -714,7 +751,12 @@ window.__ModuleLoader__.load({
714
751
  .dshdb-statuspop-name{font-size:12px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
715
752
  .dshdb-statuspop-detail{font-size:10.5px;color:var(--dsw-alias-label-tertiary);line-height:1.4;word-break:break-word}
716
753
  .dshdb-statuspop-row:has(.dshdb-dot.bad) .dshdb-statuspop-detail{color:var(--dsw-alias-state-error-primary)}
717
- .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}
718
760
  .dshdb-cv *{box-sizing:border-box}
719
761
  .dshdb-cvhead{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;flex:0 0 auto}
720
762
  .dshdb-cvtitlewrap{display:flex;align-items:center;gap:10px;min-width:0}
@@ -735,6 +777,38 @@ window.__ModuleLoader__.load({
735
777
  .dshdb-cv .dshdb-pager{padding:7px 12px}
736
778
  .dshdb-cv .dshdb-sqlpane{padding:12px 14px;gap:10px;height:100%}
737
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}
738
812
  .dshdb-cvempty{flex:1;display:flex;align-items:center;justify-content:center;padding:26px;min-height:300px}
739
813
  .dshdb-cvloading{flex:1;display:flex;align-items:center;justify-content:center;min-height:300px}
740
814
  .dshdb-cvmessage{padding:6px 0;flex:0 0 auto}
@@ -1079,6 +1153,10 @@ window.__ModuleLoader__.load({
1079
1153
  var [sqlError, setSqlError] = React.useState(null);
1080
1154
  var [sqlLoading, setSqlLoading] = React.useState(false);
1081
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({});
1082
1160
  var [dialog, setDialog] = React.useState(null);
1083
1161
  var [pendingDelete, setPendingDelete] = React.useState(null);
1084
1162
  var [busy, setBusy] = React.useState(false);
@@ -1101,6 +1179,52 @@ window.__ModuleLoader__.load({
1101
1179
  return function () { alive = false; };
1102
1180
  }, []);
1103
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
+
1104
1228
  React.useEffect(function () {
1105
1229
  if (!activeId) { setTables([]); setSelectedTable(null); setSchema(null); setResult(null); return; }
1106
1230
  var alive = true;
@@ -1445,6 +1569,62 @@ window.__ModuleLoader__.load({
1445
1569
  ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
1446
1570
  );
1447
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
+
1448
1628
  main = h("section", { className: "dshdb-main" },
1449
1629
  h("div", { className: "dshdb-mainhead" },
1450
1630
  h("div", { className: "dshdb-mainheadtop" },
@@ -1472,11 +1652,12 @@ window.__ModuleLoader__.load({
1472
1652
  ),
1473
1653
  h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
1474
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")),
1475
- 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"))
1476
1657
  )
1477
1658
  ),
1478
- tab === "data" ? dataPane : sqlPane,
1479
- h("div", { className: "dshdb-chatbar" },
1659
+ tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane,
1660
+ tab === "history" ? null : h("div", { className: "dshdb-chatbar" },
1480
1661
  h(SparkIcon, { size: 14 }),
1481
1662
  h("div", null, h("b", null, t("inChat") + " · "), t("inChatHint"))
1482
1663
  )
@@ -1844,6 +2025,47 @@ window.__ModuleLoader__.load({
1844
2025
  }
1845
2026
 
1846
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
+
1847
2069
  function ConversationDbView(props) {
1848
2070
  var t = props.t || function (key) { return key; };
1849
2071
  var sessionId = props.sessionId;
@@ -1870,6 +2092,15 @@ window.__ModuleLoader__.load({
1870
2092
  var [sqlResult, setSqlResult] = React.useState(null);
1871
2093
  var [sqlError, setSqlError] = React.useState(null);
1872
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);
1873
2104
  var [dialog, setDialog] = React.useState(null);
1874
2105
  var [busy, setBusy] = React.useState(false);
1875
2106
  var [error, setError] = React.useState(null);
@@ -1910,6 +2141,44 @@ window.__ModuleLoader__.load({
1910
2141
  return function () { alive = false; };
1911
2142
  }, [activeId, sessionId]);
1912
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
+
1913
2182
  function selectTable(connectionId, name, nextPage, internal) {
1914
2183
  setSelectedTable(name);
1915
2184
  setPage(nextPage || 0);
@@ -1972,7 +2241,10 @@ window.__ModuleLoader__.load({
1972
2241
  setSqlResult({ readOnly: readOnly, data: data });
1973
2242
  }).catch(function (failure) {
1974
2243
  setSqlError(interpolate(t("queryFailed"), { error: failure.message }));
1975
- }).finally(function () { setSqlLoading(false); });
2244
+ }).finally(function () {
2245
+ setSqlLoading(false);
2246
+ loadHistory();
2247
+ });
1976
2248
  }
1977
2249
 
1978
2250
  async function openDemo() {
@@ -2180,6 +2452,75 @@ window.__ModuleLoader__.load({
2180
2452
  ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
2181
2453
  );
2182
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
+
2183
2524
  return h("div", { className: "dshdb-cv" },
2184
2525
  h("div", { className: "dshdb-cvhead" },
2185
2526
  h("div", { className: "dshdb-cvtitlewrap" },
@@ -2212,10 +2553,11 @@ window.__ModuleLoader__.load({
2212
2553
  ),
2213
2554
  h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
2214
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")),
2215
- 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"))
2216
2558
  ),
2217
2559
  error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
2218
- h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : sqlPane),
2560
+ h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane),
2219
2561
  dialogNode
2220
2562
  );
2221
2563
  }
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.2",
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",