dsh-composer-plus 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/client.js +130 -28
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -21,7 +21,7 @@ dsh plugin --profile web add dsh-composer-plus
21
21
 
22
22
  Then restart dsh. (The CLI can't install into the desktop profile; the app manages it.)
23
23
 
24
- > dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@0.9.0`.
24
+ > dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@1.0.0`.
25
25
 
26
26
  ## Features
27
27
 
@@ -40,6 +40,10 @@ Then restart dsh. (The CLI can't install into the desktop profile; the app manag
40
40
 
41
41
  All text — buttons, tooltips, notices, notifications and the Continue message — follows the language selected in dsh (English or Chinese, the two languages dsh ships). Switching the language in dsh settings updates it live.
42
42
 
43
+ ## Settings
44
+
45
+ Every feature can be switched off on its own: dsh **Settings → Composer Plus**. Each row has a switch and a short description; changes apply immediately, without a restart, and are remembered on that device. Everything is on by default.
46
+
43
47
  ## Keyboard shortcuts
44
48
 
45
49
  | Key | Where | Action |
@@ -65,7 +69,7 @@ Tested on dsh `0.2.0-rc.2` (desktop and web) and `0.2.1-alpha.2` (web). CI runs
65
69
 
66
70
  - **No notifications on macOS:** System Settings → Notifications → DeepSeek Harness → allow. Choose **Alerts** to keep them on screen.
67
71
  - **▶, queue grips, project status, Allow for this chat or right-click missing after a dsh update:** these attach to dsh's current HTML because dsh has no slot for them yet. If dsh changes that markup they hide themselves; nothing else breaks. Please open an [issue](https://github.com/furkancak1r/dsh-composer-plus/issues).
68
- - **Install fails right after a release:** add the version (`dsh-composer-plus@0.9.0`), see the install note above.
72
+ - **Install fails right after a release:** add the version (`dsh-composer-plus@1.0.0`), see the install note above.
69
73
 
70
74
  ## Limits
71
75
 
package/client.js CHANGED
@@ -799,10 +799,49 @@ window.__ModuleLoader__.load({
799
799
  return React.createElement('section', { 'data-composer-plus-changes': '', style: { height: '100%', overflowY: 'auto', fontSize: 13 } }, header, body);
800
800
  }
801
801
 
802
+ /** Feature switches, in Settings order. Every feature is on until switched off. */
803
+ const FEATURES = ['changesPanel', 'projectStatus', 'activeProjectsFirst', 'notifications', 'allowForChat', 'continueButton',
804
+ 'inputHistory', 'planToggle', 'queueReorder', 'copyDeeplink'];
805
+ const TOGGLES_KEY = 'dsh-composer-plus.features';
806
+ /** `{ [feature]: false }` for switched-off features, kept in the app's own browser storage (per device). */
807
+ function createToggles() {
808
+ let state = {};
809
+ try { const raw = localStorage.getItem(TOGGLES_KEY); if (raw !== null) state = JSON.parse(raw) ?? {}; } catch { state = {}; }
810
+ const listeners = new Set();
811
+ return {
812
+ getSnapshot: () => state,
813
+ subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn); }; },
814
+ update: (mutate) => {
815
+ const next = { ...state }; mutate(next);
816
+ for (const key of Object.keys(next)) if (next[key] !== false) delete next[key];
817
+ state = next;
818
+ try { localStorage.setItem(TOGGLES_KEY, JSON.stringify(state)); } catch { /* storage full or blocked: keep in memory */ }
819
+ for (const fn of [...listeners]) fn();
820
+ },
821
+ };
822
+ }
823
+ /** Settings → Composer Plus: one switch per feature, styled like dsh's own preference rows. */
824
+ function FeatureSettings({ useComposerPlusToggles, setFeature, t }) {
825
+ const enabled = useComposerPlusToggles((state) => state);
826
+ const row = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '16px 0',
827
+ borderBottom: '.5px solid var(--dsw-alias-border-l2)' };
828
+ return React.createElement('div', { 'data-composer-plus-settings': '' },
829
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12, lineHeight: '18px', paddingBottom: 4 } }, t('settings.intro')),
830
+ ...FEATURES.map((id) => React.createElement('div', { key: id, style: row },
831
+ React.createElement('div', null,
832
+ React.createElement('div', { style: { fontSize: 14, lineHeight: '20px' } }, t(`feature.${id}`)),
833
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: 4, fontSize: 12, lineHeight: '18px' } }, t(`feature.${id}.description`))),
834
+ React.createElement(primitives.Switch, { checked: enabled[id] !== false, label: t(`feature.${id}`), onChange: (on) => setFeature(id, on) }))));
835
+ }
836
+
802
837
  return {
803
838
  inject: ['slots', 'sessions', 'locale', 'remote', 'remote.commands'],
804
839
  apply(ctx) {
805
- ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
840
+ // Each feature installs through `feature(id, setup)`: setup returns its disposers, and a switch in
841
+ // Settings → Composer Plus removes or reinstalls just that feature, live.
842
+ const features = [];
843
+ const feature = (id, setup) => { features.push({ id, setup, off: null }); };
844
+ feature('queueReorder', () => [ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
806
845
  name: 'conversation.input.dock',
807
846
  id: 'queue-reorder',
808
847
  order: 21,
@@ -815,8 +854,8 @@ window.__ModuleLoader__.load({
815
854
  notify: (level, text) => { try { conversation.input.for(actx).notify(level, text); } catch { /* notice is best-effort */ } },
816
855
  };
817
856
  },
818
- }, QueueReorder));
819
- ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
857
+ }, QueueReorder))]);
858
+ feature('planToggle', () => [ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
820
859
  name: 'conversation.input.right',
821
860
  id: 'composer-plan-toggle',
822
861
  order: 999,
@@ -832,15 +871,37 @@ window.__ModuleLoader__.load({
832
871
  notify: (level, text) => { try { actx.get('conversation').input.for(actx).notify(level, text); } catch { /* notice is best-effort */ } },
833
872
  };
834
873
  },
835
- }, PlanToggle));
836
- ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
874
+ }, PlanToggle))]);
875
+ feature('continueButton', () => [ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
837
876
  name: 'conversation.input.right',
838
877
  id: 'composer-continue',
839
878
  order: 1000,
840
879
  locale: LOCALE_NS,
841
- }, ContinueButton));
880
+ }, ContinueButton))]);
842
881
  if (ctx.locale !== undefined) ctx.effect(() => ctx.locale.register(LOCALE_NS, {
843
882
  en: {
883
+ 'settings.nav': 'Composer Plus',
884
+ 'settings.intro': 'Turn individual dsh-composer-plus features on or off. Changes apply immediately on this device.',
885
+ 'feature.changesPanel': 'Changes panel',
886
+ 'feature.changesPanel.description': 'Right sidebar page with uncommitted, staged and branch changes and their diffs.',
887
+ 'feature.projectStatus': 'Project status',
888
+ 'feature.projectStatus.description': 'Show the running / waiting / done marker of a project\'s chats on the project row.',
889
+ 'feature.activeProjectsFirst': 'Active projects first',
890
+ 'feature.activeProjectsFirst.description': 'Move a project to the top of the sidebar when one of its chats runs or needs you.',
891
+ 'feature.notifications': 'Background notifications',
892
+ 'feature.notifications.description': 'Notify when a chat finishes or needs you while dsh is in the background, with a Dock badge.',
893
+ 'feature.allowForChat': 'Allow for this chat',
894
+ 'feature.allowForChat.description': 'Add “Allow for this chat” to Auto Review approvals and auto-allow granted tools.',
895
+ 'feature.continueButton': 'One-click Continue',
896
+ 'feature.continueButton.description': 'Turn Send into ▶ after a stopped turn; one click sends “continue”.',
897
+ 'feature.inputHistory': 'Input history',
898
+ 'feature.inputHistory.description': 'Use ↑ / ↓ in an empty composer to reuse messages you sent.',
899
+ 'feature.planToggle': 'Shift+Tab plan mode',
900
+ 'feature.planToggle.description': 'Shift+Tab adds or removes /plan, or leaves plan mode.',
901
+ 'feature.queueReorder': 'Queue drag-and-drop',
902
+ 'feature.queueReorder.description': 'Reorder queued messages by dragging their grip.',
903
+ 'feature.copyDeeplink': 'Copy deeplink',
904
+ 'feature.copyDeeplink.description': 'Right-click a chat in the sidebar to copy a link to it.',
844
905
  'project.running': '{count} chat(s) running',
845
906
  'project.approval': 'A chat is waiting for approval',
846
907
  'project.plan-review': 'A plan is waiting for review',
@@ -886,6 +947,28 @@ window.__ModuleLoader__.load({
886
947
  'changes.refresh': 'Refresh',
887
948
  },
888
949
  zh: {
950
+ 'settings.nav': 'Composer Plus',
951
+ 'settings.intro': '单独开启或关闭 dsh-composer-plus 的各项功能。更改会立即在本设备生效。',
952
+ 'feature.changesPanel': '改动面板',
953
+ 'feature.changesPanel.description': '右侧边栏页面,显示未提交、已暂存和分支改动及其差异。',
954
+ 'feature.projectStatus': '项目状态',
955
+ 'feature.projectStatus.description': '在项目行上显示其对话的进行中 / 等待 / 完成标记。',
956
+ 'feature.activeProjectsFirst': '活跃项目置顶',
957
+ 'feature.activeProjectsFirst.description': '当项目中的对话正在运行或需要你处理时,将该项目移到侧边栏顶部。',
958
+ 'feature.notifications': '后台通知',
959
+ 'feature.notifications.description': 'dsh 在后台时,对话完成或需要你处理时发出通知,并显示程序坞角标。',
960
+ 'feature.allowForChat': '本对话内允许',
961
+ 'feature.allowForChat.description': '在 Auto Review 审批中添加“本对话内允许”,并自动允许已授权的工具。',
962
+ 'feature.continueButton': '一键继续',
963
+ 'feature.continueButton.description': '回合停止后,发送按钮变为 ▶,点击即发送“继续”。',
964
+ 'feature.inputHistory': '输入历史',
965
+ 'feature.inputHistory.description': '在空输入框中按 ↑ / ↓ 复用已发送的消息。',
966
+ 'feature.planToggle': 'Shift+Tab 计划模式',
967
+ 'feature.planToggle.description': 'Shift+Tab 添加或移除 /plan,或退出计划模式。',
968
+ 'feature.queueReorder': '队列拖拽排序',
969
+ 'feature.queueReorder.description': '拖动排队消息的把手以调整顺序。',
970
+ 'feature.copyDeeplink': '复制深层链接',
971
+ 'feature.copyDeeplink.description': '在侧边栏右键点击对话以复制其链接。',
889
972
  'project.running': '{count} 个对话进行中',
890
973
  'project.approval': '有对话等待审批',
891
974
  'project.plan-review': '有计划等待审阅',
@@ -931,29 +1014,24 @@ window.__ModuleLoader__.load({
931
1014
  'changes.refresh': '刷新',
932
1015
  },
933
1016
  }), 'dsh-composer-plus: dictionaries');
934
- ctx.slots.inject('shell.overlay', () => ctx.slots.register({
1017
+ feature('projectStatus', () => [ctx.slots.inject('shell.overlay', () => ctx.slots.register({
935
1018
  name: 'shell.overlay',
936
1019
  id: 'project-status',
937
1020
  locale: LOCALE_NS,
938
- }, ProjectStatusMarkers));
939
- ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
940
- name: 'conversation.session.header.utilities',
941
- id: 'chat-tool-grants',
942
- order: 0,
943
- locale: LOCALE_NS,
944
- }, ChatToolGrants));
945
- if (ctx.inject !== undefined) ctx.inject(['uiSession', 'uiWorkspace', 'sessions'], (scope) => {
1021
+ }, ProjectStatusMarkers))]);
1022
+ const injectScope = (deps, setup) => (ctx.inject === undefined ? [] : [ctx.inject(deps, setup)]);
1023
+ feature('notifications', () => injectScope(['uiSession', 'uiWorkspace', 'sessions'], (scope) => {
946
1024
  const t = ctx.locale.bind(LOCALE_NS);
947
1025
  scope.effect(() => watchAttention(scope.uiSession.sessionStatus, scope.sessions.list, t,
948
1026
  (sessionId) => scope.uiWorkspace.openSession(sessionId)), 'dsh-composer-plus: attention notifications');
949
1027
  // First use asks the OS once; a denied permission stays denied (re-enable it in System Settings).
950
1028
  if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission().catch(() => {});
951
- });
952
- if (ctx.inject !== undefined) ctx.inject(['workspaces', 'uiSession', 'sessions'], (scope) => {
1029
+ }));
1030
+ feature('activeProjectsFirst', () => injectScope(['workspaces', 'uiSession', 'sessions'], (scope) => {
953
1031
  scope.effect(() => watchActiveProjects(scope.workspaces, scope.sessions.list, scope.uiSession.sessionStatus),
954
1032
  'dsh-composer-plus: active projects first');
955
- });
956
- if (ctx.inject !== undefined) ctx.inject(['sidebarRight', 'sidebarRightTabs'], (scope) => {
1033
+ }));
1034
+ feature('changesPanel', () => injectScope(['sidebarRight', 'sidebarRightTabs'], (scope) => {
957
1035
  const t = ctx.locale.bind(LOCALE_NS);
958
1036
  scope.effect(() => scope.sidebarRightTabs.register({
959
1037
  id: CHANGES_ID,
@@ -972,8 +1050,13 @@ window.__ModuleLoader__.load({
972
1050
  openLastTurn: (sessionId, { seq, turn }) => scope.sidebarRight.openResource(`dsh-resource://changes-review/session/${encodeURIComponent(sessionId)}/${seq}/${turn}`),
973
1051
  }),
974
1052
  }, ChangesTab)), 'dsh-composer-plus: changes tab body');
975
- });
976
- if (ctx.inject !== undefined) ctx.inject(['uiSession'], (scope) => {
1053
+ }));
1054
+ feature('allowForChat', () => [ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
1055
+ name: 'conversation.session.header.utilities',
1056
+ id: 'chat-tool-grants',
1057
+ order: 0,
1058
+ locale: LOCALE_NS,
1059
+ }, ChatToolGrants)), ...injectScope(['uiSession'], (scope) => {
977
1060
  const status = scope.uiSession.sessionStatus;
978
1061
  // Deferred so answering never re-enters the status notification that triggered it.
979
1062
  const scan = () => { queueMicrotask(() => answerGranted(status.getSnapshot())); };
@@ -983,25 +1066,44 @@ window.__ModuleLoader__.load({
983
1066
  scan();
984
1067
  return () => { offStatus(); offGrants(); };
985
1068
  }, 'dsh-composer-plus: chat tool grants');
986
- });
1069
+ })]);
987
1070
  if (ctx.effect !== undefined) ctx.effect(() => () => { grants.clear(); grantsChanged(); }, 'dsh-composer-plus: forget grants on unload');
988
1071
  if (typeof fetch === 'function') loadGrants();
989
- ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
1072
+ feature('copyDeeplink', () => [ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
990
1073
  name: 'sidebar.workspaces.session.menu.item',
991
1074
  id: 'copy-deeplink',
992
1075
  order: 350,
993
1076
  locale: LOCALE_NS,
994
- }, CopyDeeplinkItem));
995
- ctx.slots.inject('sidebar.workspaces.session.row.action', () => ctx.slots.register({
1077
+ }, CopyDeeplinkItem)), ctx.slots.inject('sidebar.workspaces.session.row.action', () => ctx.slots.register({
996
1078
  name: 'sidebar.workspaces.session.row.action',
997
1079
  id: 'session-context-menu',
998
1080
  order: 0,
999
- }, RowContextMenu));
1000
- ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
1081
+ }, RowContextMenu))]);
1082
+ feature('inputHistory', () => [ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
1001
1083
  name: 'conversation.composer.dock',
1002
1084
  id: 'composer-history',
1003
1085
  order: 20,
1004
- }, ComposerHistory));
1086
+ }, ComposerHistory))]);
1087
+ const toggles = createToggles();
1088
+ const apply = () => {
1089
+ const enabled = toggles.getSnapshot();
1090
+ for (const f of features) {
1091
+ const on = enabled[f.id] !== false;
1092
+ if (on && f.off === null) { const offs = f.setup(); f.off = () => { for (const off of offs.reverse()) off?.(); }; }
1093
+ else if (!on && f.off !== null) { f.off(); f.off = null; }
1094
+ }
1095
+ };
1096
+ apply();
1097
+ const offToggles = toggles.subscribe(apply);
1098
+ if (ctx.effect !== undefined) ctx.effect(() => () => { offToggles(); for (const f of features) if (f.off !== null) { f.off(); f.off = null; } }, 'dsh-composer-plus: feature toggles');
1099
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1100
+ name: 'settings.section',
1101
+ id: 'composer-plus',
1102
+ order: 90,
1103
+ label: () => (ctx.locale === undefined ? 'Composer Plus' : ctx.locale.bind(LOCALE_NS)('settings.nav')),
1104
+ locale: LOCALE_NS,
1105
+ inject: () => ({ hooks: { composerPlusToggles: toggles }, setFeature: (id, on) => toggles.update((draft) => { draft[id] = on; }) }),
1106
+ }, FeatureSettings));
1005
1107
  },
1006
1108
  };
1007
1109
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-composer-plus",
3
- "version": "0.9.0",
3
+ "version": "1.0.0",
4
4
  "description": "DeepSeek Harness helpers: Codex-style Changes panel, input history, Shift+Tab /plan, one-click Continue, queue drag-and-drop, Copy deeplink, Auto Review allow-for-chat, background notifications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "LICENSE"
26
26
  ],
27
27
  "scripts": {
28
- "test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs && node test/project-status.test.mjs"
28
+ "test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs && node test/project-status.test.mjs && node test/toggles.test.mjs"
29
29
  },
30
30
  "dsh": {
31
31
  "bundle": {