@ran-sh/dsh-crew 1.3.2 → 1.5.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/lib/client.js CHANGED
@@ -570,8 +570,10 @@ const copy = {
570
570
  operation: "操作",
571
571
  archive: "归档(可恢复)",
572
572
  delete: "删除(不可恢复)",
573
- scope: "时间范围",
574
- all: "全部",
573
+ scope: "范围",
574
+ all: "全部(含你自己的会话)",
575
+ crew: "仅 Crew 创建(推荐)",
576
+ worktree: "仅 Crew 工作树",
575
577
  before: "指定时间之前",
576
578
  time: "创建时间早于",
577
579
  timeHint: "按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。",
@@ -610,8 +612,10 @@ const copy = {
610
612
  operation: "Action",
611
613
  archive: "Archive (restorable)",
612
614
  delete: "Delete (permanent)",
613
- scope: "Time range",
614
- all: "All",
615
+ scope: "Scope",
616
+ all: "Everything (includes your own sessions)",
617
+ crew: "Crew-created only (recommended)",
618
+ worktree: "Crew worktrees only",
615
619
  before: "Before a date",
616
620
  time: "Created before",
617
621
  timeHint: "Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.",
@@ -649,7 +653,7 @@ function HistoryPanel({ locale }) {
649
653
  const t = copy[locale === "zh" ? "zh" : "en"];
650
654
  const [open, setOpen] = (0, react.useState)(false);
651
655
  const [operation, setOperation] = (0, react.useState)("archive");
652
- const [scope, setScope] = (0, react.useState)("all");
656
+ const [scope, setScope] = (0, react.useState)("crew");
653
657
  const [before, setBefore] = (0, react.useState)("");
654
658
  const [preview, setPreview] = (0, react.useState)(null);
655
659
  const [status, setStatus] = (0, react.useState)({ phase: "IDLE" });
@@ -793,13 +797,24 @@ function HistoryPanel({ locale }) {
793
797
  setScope(e.target.value);
794
798
  invalidate();
795
799
  },
796
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
797
- value: "all",
798
- children: t.all
799
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
800
- value: "before",
801
- children: t.before
802
- })]
800
+ children: [
801
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
802
+ value: "crew",
803
+ children: t.crew
804
+ }),
805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
806
+ value: "worktree",
807
+ children: t.worktree
808
+ }),
809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
810
+ value: "all",
811
+ children: t.all
812
+ }),
813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
814
+ value: "before",
815
+ children: t.before
816
+ })
817
+ ]
803
818
  })
804
819
  ] })]
805
820
  }),
@@ -964,6 +979,148 @@ function HistoryPanel({ locale }) {
964
979
  });
965
980
  }
966
981
  //#endregion
982
+ //#region src/model-schedule.mjs
983
+ /** Per-model restriction strength. Absent from the list means `off`. */
984
+ const MODEL_SCHEDULE_MODES = Object.freeze([
985
+ "off",
986
+ "warn",
987
+ "block"
988
+ ]);
989
+ /** Peak means "expensive". Off-peak is every hour outside the windows. */
990
+ const DEFAULT_PEAK_WINDOWS = Object.freeze([Object.freeze({
991
+ start: "09:00",
992
+ end: "12:00"
993
+ }), Object.freeze({
994
+ start: "14:00",
995
+ end: "18:00"
996
+ })]);
997
+ const DEFAULT_PEAK_WEEKDAYS = Object.freeze([
998
+ 1,
999
+ 2,
1000
+ 3,
1001
+ 4,
1002
+ 5
1003
+ ]);
1004
+ const MAX_OFFSET_MINUTES = 840;
1005
+ /** "HH:MM" in 24-hour form to minutes past local midnight, or null. */
1006
+ function clockMinutes(value) {
1007
+ if (typeof value !== "string") return null;
1008
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
1009
+ if (!match) return null;
1010
+ const hours = Number(match[1]);
1011
+ const minutes = Number(match[2]);
1012
+ if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return null;
1013
+ if (hours > 23 || minutes > 59) return null;
1014
+ return hours * 60 + minutes;
1015
+ }
1016
+ function offsetMinutes(value, fallback) {
1017
+ if (value === void 0 || value === null) return fallback;
1018
+ if (typeof value === "number" ? !Number.isInteger(value) : !/^-?\d+$/.test(String(value).trim())) return null;
1019
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value).trim(), 10);
1020
+ if (Math.abs(parsed) > MAX_OFFSET_MINUTES) return null;
1021
+ return parsed;
1022
+ }
1023
+ function weekdayList(value, fallback) {
1024
+ if (value === void 0 || value === null) return [...fallback];
1025
+ if (!Array.isArray(value)) return null;
1026
+ return [...new Set(value.filter((day) => Number.isInteger(day) && day >= 0 && day <= 6))].sort((a, b) => a - b);
1027
+ }
1028
+ function peakWindows(value, fallback) {
1029
+ if (value === void 0 || value === null) return fallback.map((window) => ({ ...window }));
1030
+ if (!Array.isArray(value)) return null;
1031
+ const windows = [];
1032
+ for (const entry of value) {
1033
+ const start = clockMinutes(entry?.start);
1034
+ const end = clockMinutes(entry?.end);
1035
+ if (start === null || end === null || start === end) continue;
1036
+ windows.push({
1037
+ start: entry.start.trim(),
1038
+ end: entry.end.trim()
1039
+ });
1040
+ }
1041
+ return windows;
1042
+ }
1043
+ function modelModes(value) {
1044
+ if (!Array.isArray(value)) return [];
1045
+ const seen = /* @__PURE__ */ new Set();
1046
+ const modes = [];
1047
+ for (const entry of value) {
1048
+ const provider = typeof entry?.provider === "string" ? entry.provider.trim() : "";
1049
+ const model = typeof entry?.model === "string" ? entry.model.trim() : "";
1050
+ const mode = typeof entry?.mode === "string" ? entry.mode.trim() : "";
1051
+ if (!provider || !model || !MODEL_SCHEDULE_MODES.includes(mode)) continue;
1052
+ if (mode === "off") continue;
1053
+ const key = `${provider}\0${model}`;
1054
+ if (seen.has(key)) continue;
1055
+ seen.add(key);
1056
+ modes.push({
1057
+ provider,
1058
+ model,
1059
+ mode
1060
+ });
1061
+ }
1062
+ return modes;
1063
+ }
1064
+ /**
1065
+ * Coerce a stored or user-supplied schedule into the canonical shape.
1066
+ *
1067
+ * Lenient toward *missing* fields, strict toward *broken* ones. A missing field
1068
+ * gets its documented default; a field that is present but unusable (a string
1069
+ * where a list belongs, a non-integer offset) makes the whole schedule
1070
+ * non-restricting instead of substituting defaults. Substituting would let
1071
+ * accidental corruption switch on restrictions the operator never asked for.
1072
+ */
1073
+ function normalizeModelSchedule(raw) {
1074
+ const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1075
+ const models = modelModes(source.models);
1076
+ const offset = offsetMinutes(source.timezone_offset_minutes, 480);
1077
+ const weekdays = weekdayList(source.weekdays, DEFAULT_PEAK_WEEKDAYS);
1078
+ const windows = peakWindows(source.peak_windows, DEFAULT_PEAK_WINDOWS);
1079
+ if (offset === null || weekdays === null || windows === null) return {
1080
+ timezone_offset_minutes: offset === null ? 480 : offset,
1081
+ weekdays: [],
1082
+ peak_windows: windows === null ? [] : windows,
1083
+ models
1084
+ };
1085
+ return {
1086
+ timezone_offset_minutes: offset,
1087
+ weekdays,
1088
+ peak_windows: windows,
1089
+ models
1090
+ };
1091
+ }
1092
+ /** Local wall clock for an instant under a fixed offset. */
1093
+ function localParts(at, offset) {
1094
+ const shifted = new Date(at.getTime() + offset * 6e4);
1095
+ return {
1096
+ weekday: shifted.getUTCDay(),
1097
+ minutes: shifted.getUTCHours() * 60 + shifted.getUTCMinutes()
1098
+ };
1099
+ }
1100
+ function inWindow(minutes, start, end) {
1101
+ return start < end ? minutes >= start && minutes < end : minutes >= start || minutes < end;
1102
+ }
1103
+ /**
1104
+ * Whether one instant falls in a peak window.
1105
+ *
1106
+ * A window crossing midnight also belongs to the weekday it starts on, so the
1107
+ * early-morning half is checked against the previous day.
1108
+ */
1109
+ function isPeakAt(schedule, at = /* @__PURE__ */ new Date()) {
1110
+ const normalized = normalizeModelSchedule(schedule);
1111
+ if (normalized.weekdays.length === 0) return false;
1112
+ const { weekday, minutes } = localParts(at, normalized.timezone_offset_minutes);
1113
+ for (const window of normalized.peak_windows) {
1114
+ const start = clockMinutes(window.start);
1115
+ const end = clockMinutes(window.end);
1116
+ if (start === null || end === null) continue;
1117
+ if (!inWindow(minutes, start, end)) continue;
1118
+ const owner = start < end || minutes >= start ? weekday : (weekday + 6) % 7;
1119
+ if (normalized.weekdays.includes(owner)) return true;
1120
+ }
1121
+ return false;
1122
+ }
1123
+ //#endregion
967
1124
  //#region src/client/index.tsx
968
1125
  const inject$1 = ["slots", "locale"];
969
1126
  const API = "/_dsh/dsh-crew";
@@ -1020,6 +1177,7 @@ const COPY = {
1020
1177
  flash: "Worker / Flash",
1021
1178
  pro: "Reviewer / Pro",
1022
1179
  dispatch: "模型优先级与派发",
1180
+ schedule: "波峰波谷调度",
1023
1181
  adaptive: "自适应路由",
1024
1182
  runtime: "运行 / 生效边界",
1025
1183
  multimodal: "视觉与生图",
@@ -1291,6 +1449,22 @@ const COPY = {
1291
1449
  adaptiveWindow: "健康窗口",
1292
1450
  adaptiveSamples: "最少样本",
1293
1451
  adaptiveBoundary: "下一个工作流生效",
1452
+ scheduleTitle: "波峰波谷调度",
1453
+ scheduleHint: "按本地时间限制指定模型:波峰时被“阻止”的模型会被跳过,由下一个候选接替任务;“提醒”的仍会使用并在选择轨迹中标记。未列出的模型不受限制。默认时段为 DeepSeek 公布的波峰时间(工作日 09:00-12:00、14:00-18:00)。",
1454
+ scheduleTimezone: "时区(相对 UTC)",
1455
+ scheduleWindows: "波峰时段",
1456
+ schedulePeak: "现在处于波峰",
1457
+ scheduleOffPeak: "现在处于波谷",
1458
+ scheduleAddWindow: "添加时段",
1459
+ scheduleStart: "开始",
1460
+ scheduleEnd: "结束",
1461
+ scheduleBlock: "阻止",
1462
+ scheduleWarn: "提醒",
1463
+ scheduleOff: "关闭",
1464
+ scheduleRestricted: "受限模型",
1465
+ scheduleNoRestricted: "还没有受限模型;未列出的模型不受时间限制。",
1466
+ scheduleWeekdays: "生效工作日",
1467
+ scheduleBoundary: "下次派发生效",
1294
1468
  modelActivity: "模型活动概览",
1295
1469
  modelActivityHint: "聚合本 Hub 内存中最近 500 个含真实调用证据的任务;每个模型按任务计数(不是推测底层轮次),最多显示 50 个模型。不保存提示词、结果或凭据。",
1296
1470
  noModelActivity: "还没有真实模型活动。",
@@ -1364,6 +1538,7 @@ const COPY = {
1364
1538
  flash: "Worker / Flash",
1365
1539
  pro: "Reviewer / Pro",
1366
1540
  dispatch: "Model priority & dispatch",
1541
+ schedule: "Peak / off-peak scheduling",
1367
1542
  adaptive: "Adaptive routing",
1368
1543
  runtime: "Runtime / activation boundaries",
1369
1544
  multimodal: "Vision & image generation",
@@ -1635,6 +1810,22 @@ const COPY = {
1635
1810
  adaptiveWindow: "Health window",
1636
1811
  adaptiveSamples: "Minimum samples",
1637
1812
  adaptiveBoundary: "Effective for the next workflow",
1813
+ scheduleTitle: "Peak / off-peak scheduling",
1814
+ scheduleHint: "Restricts chosen models by local wall clock. During peak, a \"block\" model is skipped and the next candidate serves the job; a \"warn\" model is still used and flagged in the selection trace. Models not listed are unrestricted. Defaults are DeepSeek's published peak hours (weekdays 09:00-12:00 and 14:00-18:00).",
1815
+ scheduleTimezone: "Timezone (relative to UTC)",
1816
+ scheduleWindows: "Peak windows",
1817
+ schedulePeak: "Peak hours now",
1818
+ scheduleOffPeak: "Off-peak now",
1819
+ scheduleAddWindow: "Add window",
1820
+ scheduleStart: "Start",
1821
+ scheduleEnd: "End",
1822
+ scheduleBlock: "Block",
1823
+ scheduleWarn: "Warn",
1824
+ scheduleOff: "Off",
1825
+ scheduleRestricted: "Restricted models",
1826
+ scheduleNoRestricted: "No restricted models yet; unlisted models have no time limit.",
1827
+ scheduleWeekdays: "Days the windows apply",
1828
+ scheduleBoundary: "Effective for the next dispatch",
1638
1829
  modelActivity: "Model activity overview",
1639
1830
  modelActivityHint: "Aggregates the latest 500 in-memory Hub jobs with real model-activity evidence; each model is counted once per qualifying job (not an inferred turn count). Shows at most 50 models; prompts, results, and credentials are never stored here.",
1640
1831
  noModelActivity: "No real model activity yet.",
@@ -2964,6 +3155,74 @@ function WorkersPanel({ ctx }) {
2964
3155
  ordering
2965
3156
  } } });
2966
3157
  };
3158
+ const schedule = normalizeModelSchedule(config?.model_schedule);
3159
+ const saveSchedule = (candidate) => {
3160
+ const next = normalizeModelSchedule(candidate);
3161
+ setConfig((current) => ({
3162
+ ...current,
3163
+ model_schedule: next
3164
+ }));
3165
+ applyPatch({ model_schedule: next });
3166
+ };
3167
+ const schedulePeakNow = isPeakAt(schedule);
3168
+ const scheduleModeOf = (ref) => schedule.models.find((entry) => entry.provider === ref.provider && entry.model === ref.model)?.mode ?? "off";
3169
+ const setScheduleMode = (ref, mode) => {
3170
+ const others = schedule.models.filter((entry) => !(entry.provider === ref.provider && entry.model === ref.model));
3171
+ saveSchedule({
3172
+ ...schedule,
3173
+ models: mode === "off" ? others : [...others, {
3174
+ ...ref,
3175
+ mode
3176
+ }]
3177
+ });
3178
+ };
3179
+ const scheduleModelRefs = (() => {
3180
+ const seen = /* @__PURE__ */ new Map();
3181
+ const add = (ref) => {
3182
+ if (ref?.provider && ref?.model) seen.set(`${ref.provider}\0${ref.model}`, {
3183
+ provider: ref.provider,
3184
+ model: ref.model
3185
+ });
3186
+ };
3187
+ for (const ref of config?.flash_model_priority ?? []) add(ref);
3188
+ for (const ref of config?.pro_model_priority ?? []) add(ref);
3189
+ for (const role of ["worker", "review"]) {
3190
+ const policy = config?.[role]?.model_policy;
3191
+ for (const ref of policy?.priority ?? []) add(ref);
3192
+ for (const ref of policy?.escalation_priority ?? []) add(ref);
3193
+ }
3194
+ add(modelCatalog?.harness_default);
3195
+ for (const provider of modelCatalog?.providers ?? []) for (const model of provider?.models ?? []) add({
3196
+ provider: provider.id,
3197
+ model: model?.id
3198
+ });
3199
+ for (const entry of schedule.models) add(entry);
3200
+ return [...seen.values()];
3201
+ })();
3202
+ const scheduleWeekdayNames = locale === "zh" ? [
3203
+ "日",
3204
+ "一",
3205
+ "二",
3206
+ "三",
3207
+ "四",
3208
+ "五",
3209
+ "六"
3210
+ ] : [
3211
+ "Sun",
3212
+ "Mon",
3213
+ "Tue",
3214
+ "Wed",
3215
+ "Thu",
3216
+ "Fri",
3217
+ "Sat"
3218
+ ];
3219
+ const toggleScheduleWeekday = (day) => {
3220
+ const days = schedule.weekdays.includes(day) ? schedule.weekdays.filter((value) => value !== day) : [...schedule.weekdays, day].sort((a, b) => a - b);
3221
+ saveSchedule({
3222
+ ...schedule,
3223
+ weekdays: days
3224
+ });
3225
+ };
2967
3226
  const modelActivity = aggregateModelInvocations(jobs);
2968
3227
  const modelState = modelCallabilityState(readinessSnapshot?.model_callability, runtimeInfo);
2969
3228
  const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
@@ -4078,6 +4337,237 @@ function WorkersPanel({ ctx }) {
4078
4337
  })
4079
4338
  ] }))
4080
4339
  }),
4340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
4341
+ sectionId: "schedule",
4342
+ title: copy.sectionNames.schedule,
4343
+ summary: sectionSummary(schedulePeakNow ? copy.schedulePeak : copy.scheduleOffPeak, `UTC${schedule.timezone_offset_minutes >= 0 ? "+" : "-"}${Math.abs(schedule.timezone_offset_minutes) / 60}`, config?.model_schedule === void 0 ? copy.scheduleOff : `${schedule.models.length} · ${copy.scheduleRestricted}`),
4344
+ expanded: !!expandedSections.schedule,
4345
+ onToggle: () => toggleSection("schedule"),
4346
+ children: block({
4347
+ t: copy.scheduleTitle,
4348
+ d: copy.scheduleHint
4349
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4351
+ style: S.field,
4352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4353
+ style: S.fieldLabel,
4354
+ children: copy.scheduleTimezone
4355
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
4356
+ value: String(schedule.timezone_offset_minutes),
4357
+ onChange: (v) => saveSchedule({
4358
+ ...schedule,
4359
+ timezone_offset_minutes: Number(v)
4360
+ }),
4361
+ options: (() => {
4362
+ const offsets = /* @__PURE__ */ new Set();
4363
+ for (let h = -14; h <= 14; h++) offsets.add(h * 60);
4364
+ for (const minutes of [
4365
+ 330,
4366
+ 345,
4367
+ 570,
4368
+ -210,
4369
+ -570
4370
+ ]) offsets.add(minutes);
4371
+ offsets.add(schedule.timezone_offset_minutes);
4372
+ return [...offsets].sort((a, b) => a - b).map((minutes) => ({
4373
+ value: String(minutes),
4374
+ label: `UTC${minutes >= 0 ? "+" : "-"}${Math.floor(Math.abs(minutes) / 60)}` + (Math.abs(minutes) % 60 ? `:${String(Math.abs(minutes) % 60).padStart(2, "0")}` : "")
4375
+ }));
4376
+ })()
4377
+ })]
4378
+ }),
4379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4380
+ style: { gridColumn: "1 / -1" },
4381
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4382
+ style: {
4383
+ ...S.fieldLabel,
4384
+ display: "block",
4385
+ marginBottom: 5
4386
+ },
4387
+ children: copy.scheduleWeekdays
4388
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4389
+ style: {
4390
+ display: "flex",
4391
+ gap: 5,
4392
+ flexWrap: "wrap"
4393
+ },
4394
+ children: scheduleWeekdayNames.map((name, day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4395
+ type: "button",
4396
+ style: {
4397
+ ...S.btn,
4398
+ padding: "3px 9px",
4399
+ opacity: schedule.weekdays.includes(day) ? 1 : .45
4400
+ },
4401
+ onClick: () => toggleScheduleWeekday(day),
4402
+ children: name
4403
+ }, day))
4404
+ })]
4405
+ }),
4406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4407
+ style: { gridColumn: "1 / -1" },
4408
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4409
+ style: {
4410
+ ...S.fieldLabel,
4411
+ display: "block",
4412
+ marginBottom: 5
4413
+ },
4414
+ children: copy.scheduleWindows
4415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4416
+ style: {
4417
+ display: "flex",
4418
+ flexDirection: "column",
4419
+ gap: 6
4420
+ },
4421
+ children: [schedule.peak_windows.map((window, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4422
+ style: {
4423
+ display: "flex",
4424
+ gap: 6,
4425
+ alignItems: "center"
4426
+ },
4427
+ children: [
4428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4429
+ type: "time",
4430
+ value: window.start,
4431
+ style: S.input,
4432
+ onChange: (event) => {
4433
+ const next = schedule.peak_windows.map((row, i) => i === index ? {
4434
+ ...row,
4435
+ start: event.target.value
4436
+ } : row);
4437
+ saveSchedule({
4438
+ ...schedule,
4439
+ peak_windows: next
4440
+ });
4441
+ }
4442
+ }),
4443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4444
+ style: { opacity: .6 },
4445
+ children: "–"
4446
+ }),
4447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4448
+ type: "time",
4449
+ value: window.end,
4450
+ style: S.input,
4451
+ onChange: (event) => {
4452
+ const next = schedule.peak_windows.map((row, i) => i === index ? {
4453
+ ...row,
4454
+ end: event.target.value
4455
+ } : row);
4456
+ saveSchedule({
4457
+ ...schedule,
4458
+ peak_windows: next
4459
+ });
4460
+ }
4461
+ }),
4462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4463
+ type: "button",
4464
+ style: {
4465
+ ...S.btn,
4466
+ padding: "2px 8px"
4467
+ },
4468
+ onClick: () => saveSchedule({
4469
+ ...schedule,
4470
+ peak_windows: schedule.peak_windows.filter((_, i) => i !== index)
4471
+ }),
4472
+ children: "×"
4473
+ })
4474
+ ]
4475
+ }, `${window.start}-${window.end}-${index}`)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4476
+ type: "button",
4477
+ style: {
4478
+ ...S.btn,
4479
+ alignSelf: "flex-start"
4480
+ },
4481
+ onClick: () => saveSchedule({
4482
+ ...schedule,
4483
+ peak_windows: [...schedule.peak_windows, {
4484
+ start: "09:00",
4485
+ end: "12:00"
4486
+ }]
4487
+ }),
4488
+ children: copy.scheduleAddWindow
4489
+ })]
4490
+ })]
4491
+ }),
4492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4493
+ style: { gridColumn: "1 / -1" },
4494
+ children: [
4495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4496
+ style: {
4497
+ ...S.fieldLabel,
4498
+ display: "block",
4499
+ marginBottom: 5
4500
+ },
4501
+ children: copy.scheduleRestricted
4502
+ }),
4503
+ scheduleModelRefs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4504
+ style: {
4505
+ fontSize: 11.5,
4506
+ opacity: .6
4507
+ },
4508
+ children: copy.scheduleNoRestricted
4509
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4510
+ style: {
4511
+ display: "flex",
4512
+ flexDirection: "column",
4513
+ gap: 5
4514
+ },
4515
+ children: scheduleModelRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4516
+ style: {
4517
+ display: "flex",
4518
+ gap: 8,
4519
+ alignItems: "center",
4520
+ padding: "5px 8px",
4521
+ border: "1px solid rgba(128,128,128,0.2)",
4522
+ borderRadius: 6
4523
+ },
4524
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4525
+ style: {
4526
+ flex: 1,
4527
+ minWidth: 0,
4528
+ fontSize: 11,
4529
+ fontFamily: "monospace",
4530
+ overflow: "hidden",
4531
+ textOverflow: "ellipsis"
4532
+ },
4533
+ children: [
4534
+ ref.provider,
4535
+ " / ",
4536
+ ref.model
4537
+ ]
4538
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
4539
+ value: scheduleModeOf(ref),
4540
+ onChange: (v) => setScheduleMode(ref, v),
4541
+ options: [
4542
+ {
4543
+ value: "off",
4544
+ label: copy.scheduleOff
4545
+ },
4546
+ {
4547
+ value: "warn",
4548
+ label: copy.scheduleWarn
4549
+ },
4550
+ {
4551
+ value: "block",
4552
+ label: copy.scheduleBlock
4553
+ }
4554
+ ]
4555
+ })]
4556
+ }, `${ref.provider}\0${ref.model}`))
4557
+ }),
4558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4559
+ style: {
4560
+ display: "block",
4561
+ fontSize: 11,
4562
+ opacity: .55,
4563
+ marginTop: 5
4564
+ },
4565
+ children: copy.scheduleBoundary
4566
+ })
4567
+ ]
4568
+ })
4569
+ ] }))
4570
+ }),
4081
4571
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
4082
4572
  sectionId: "runtime",
4083
4573
  title: copy.sectionNames.runtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -5,7 +5,7 @@ const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLL
5
5
  const copy = {
6
6
  zh: {
7
7
  title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
8
- operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '时间范围', all: '全部', before: '指定时间之前',
8
+ operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
9
9
  time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
10
10
  preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
11
11
  consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
@@ -17,7 +17,7 @@ const copy = {
17
17
  },
18
18
  en: {
19
19
  title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
20
- operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Time range', all: 'All', before: 'Before a date',
20
+ operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
21
21
  time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
22
22
  preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
23
23
  consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
@@ -33,7 +33,7 @@ export function HistoryPanel({ locale }: { locale: string }) {
33
33
  const t = copy[locale === 'zh' ? 'zh' : 'en'];
34
34
  const [open, setOpen] = useState(false);
35
35
  const [operation, setOperation] = useState('archive');
36
- const [scope, setScope] = useState('all');
36
+ const [scope, setScope] = useState('crew');
37
37
  const [before, setBefore] = useState('');
38
38
  const [preview, setPreview] = useState<any>(null);
39
39
  const [status, setStatus] = useState<any>({ phase: 'IDLE' });
@@ -81,7 +81,7 @@ export function HistoryPanel({ locale }: { locale: string }) {
81
81
  <p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
82
82
  <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
83
83
  <label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
84
- <label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
84
+ <label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
85
85
  </div>
86
86
  {scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
87
87
  <div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>