@listen1954/board 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/client.js +270 -16
- package/lib/domain.d.ts +4 -0
- package/lib/domain.js +9 -0
- package/lib/prompt.js +4 -4
- package/lib/rpc.d.ts +6 -0
- package/lib/rpc.js +3 -0
- package/lib/service.d.ts +6 -0
- package/lib/service.js +13 -0
- package/lib/tools.js +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,10 +23,12 @@ Agent 与界面使用同一个 Board 服务,所有看板读写只能通过以
|
|
|
23
23
|
|
|
24
24
|
- `board_list`:查看全局/当前会话概览,并取得列、卡片和标签 ID。
|
|
25
25
|
- `board_search`:按用户描述查找已有任务并取得卡片 ID。
|
|
26
|
-
- `board_get
|
|
27
|
-
- `board_apply
|
|
26
|
+
- `board_get`:读取已知卡片的备注、时间、标签和过程记录(含可用于删除的日志 ID)。
|
|
27
|
+
- `board_apply`:唯一写入口,创建、修改、移动、完成、恢复或删除任务、过程记录。
|
|
28
28
|
|
|
29
29
|
已有任务固定走 `board_search →(需要详情时)board_get → board_apply`;按列名新增任务走 `board_list → board_apply`。Agent 不应读取或修改插件存储 JSON、源码、安装目录或私有 HTTP 地址;接口失败时最多刷新并修正一次,仍失败就向用户报告,不得绕过接口直接改数据。
|
|
30
|
+
|
|
31
|
+
过程记录中的 `http://`、`https://` 地址可直接用系统默认浏览器打开。删除过程记录时,其附带的图片和文件也会一并删除。
|
|
30
32
|
|
|
31
33
|
## 安装
|
|
32
34
|
|
package/lib/client.js
CHANGED
|
@@ -91,6 +91,7 @@ var zh = {
|
|
|
91
91
|
logPlaceholder: "\u8BB0\u5F55\u8FDB\u5EA6\u6216\u5907\u5FD8\u2026",
|
|
92
92
|
submitLog: "\u63D0\u4EA4\u8BB0\u5F55",
|
|
93
93
|
noLogs: "\u8FD8\u6CA1\u6709\u8FC7\u7A0B\u8BB0\u5F55",
|
|
94
|
+
deleteLog: "\u5220\u9664\u8FD9\u6761\u8BB0\u5F55",
|
|
94
95
|
uploadAttachment: "\u4E0A\u4F20\u9644\u4EF6",
|
|
95
96
|
removeAttachment: "\u79FB\u9664\u9644\u4EF6\u201C{name}\u201D",
|
|
96
97
|
attachmentLimit: "\u9644\u4EF6\u6700\u591A 10 \u4E2A\uFF0C\u5355\u4E2A\u4E0D\u8D85\u8FC7 50 MB",
|
|
@@ -124,6 +125,16 @@ var zh = {
|
|
|
124
125
|
nextMonth: "\u4E0B\u4E2A\u6708",
|
|
125
126
|
time: "\u65F6\u95F4",
|
|
126
127
|
viewDetails: "\u67E5\u770B\u8BE6\u60C5",
|
|
128
|
+
timelineView: "\u4EFB\u52A1\u53EF\u89C6\u5316\u89C6\u56FE",
|
|
129
|
+
timelineTitle: "\u4EFB\u52A1\u65F6\u95F4\u8F74",
|
|
130
|
+
timelineTaskCount: "{n} \u4E2A\u65F6\u95F4\u4EFB\u52A1",
|
|
131
|
+
timelineUnscheduled: "{n} \u4E2A\u4EFB\u52A1\u672A\u8BBE\u7F6E\u65F6\u95F4",
|
|
132
|
+
timelineEmpty: "\u8FD8\u6CA1\u6709\u53EF\u5C55\u793A\u7684\u65F6\u95F4\u4EFB\u52A1",
|
|
133
|
+
timelineEmptyHint: "\u4E3A\u4EFB\u52A1\u8BBE\u7F6E\u5F00\u59CB\u6216\u7ED3\u675F\u65F6\u95F4\u540E\uFF0C\u5B83\u4F1A\u51FA\u73B0\u5728\u65F6\u95F4\u8F74\u4E0A\u3002",
|
|
134
|
+
timelineNow: "\u73B0\u5728",
|
|
135
|
+
timelineZoomIn: "\u653E\u5927\u65F6\u95F4\u8F74",
|
|
136
|
+
timelineZoomOut: "\u7F29\u5C0F\u65F6\u95F4\u8F74",
|
|
137
|
+
timelineFit: "\u9002\u5E94\u5168\u90E8\u65F6\u95F4",
|
|
127
138
|
settingUpSessionBoard: "\u6B63\u5728\u51C6\u5907\u672C\u4F1A\u8BDD\u770B\u677F\u2026",
|
|
128
139
|
cardCount: "{n} \u5F20\u5361\u7247",
|
|
129
140
|
complete: "\u6807\u8BB0\u5B8C\u6210",
|
|
@@ -216,6 +227,7 @@ var en = {
|
|
|
216
227
|
logPlaceholder: "Record progress or a memo\u2026",
|
|
217
228
|
submitLog: "Add log",
|
|
218
229
|
noLogs: "No activity yet",
|
|
230
|
+
deleteLog: "Delete this log",
|
|
219
231
|
uploadAttachment: "Upload attachment",
|
|
220
232
|
removeAttachment: "Remove attachment \u201C{name}\u201D",
|
|
221
233
|
attachmentLimit: "Up to 10 files, 50 MB each",
|
|
@@ -249,6 +261,16 @@ var en = {
|
|
|
249
261
|
nextMonth: "Next month",
|
|
250
262
|
time: "Time",
|
|
251
263
|
viewDetails: "View details",
|
|
264
|
+
timelineView: "Task timeline view",
|
|
265
|
+
timelineTitle: "Task timeline",
|
|
266
|
+
timelineTaskCount: "{n} scheduled tasks",
|
|
267
|
+
timelineUnscheduled: "{n} tasks without dates",
|
|
268
|
+
timelineEmpty: "No tasks to show on the timeline",
|
|
269
|
+
timelineEmptyHint: "Set a start or end time on a task to place it on the timeline.",
|
|
270
|
+
timelineNow: "Now",
|
|
271
|
+
timelineZoomIn: "Zoom in",
|
|
272
|
+
timelineZoomOut: "Zoom out",
|
|
273
|
+
timelineFit: "Fit entire timeline",
|
|
252
274
|
settingUpSessionBoard: "Preparing this session\u2019s board\u2026",
|
|
253
275
|
cardCount: "{n} cards",
|
|
254
276
|
complete: "Mark complete",
|
|
@@ -376,7 +398,7 @@ body[data-ds-dark-theme] .board-global, body[data-ds-dark-theme] .board-overlay,
|
|
|
376
398
|
.board-log-heading { display:flex; align-items:center; justify-content:space-between; gap:.5rem; }.board-log-heading strong { color:var(--board-text); font-size:.8125rem; line-height:1.25rem; }.board-log-heading span { color:var(--board-dim); font-size:.6875rem; }
|
|
377
399
|
.board-log-scroll { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; }.board-log-composer { position:static; flex:none; padding:.25rem; border:1px solid var(--board-border); border-radius:.5rem; background:var(--board-surface); }.board-log-compose-row { display:flex; align-items:center; min-width:0; gap:.1875rem; }.board-log-input { flex:1; min-width:0; height:1.625rem; padding:.1875rem .3125rem; border:0; outline:0; background:transparent; color:var(--board-text); font:inherit; font-size:.75rem; line-height:1rem; }.board-log-input::placeholder { color:var(--board-dim); opacity:1; }.board-log-compose-row > input[type=file] { display:none; }.board-log-compose-row .board-btn { min-height:1.625rem; padding:0 .5rem; font-size:.6875rem; }.board-log-compose-row .board-log-upload { width:1.625rem; min-height:1.625rem; }
|
|
378
400
|
.board-log-pending { display:flex; flex-wrap:wrap; gap:.375rem; margin-bottom:.375rem; padding-bottom:.375rem; border-bottom:1px solid var(--board-border); }.board-log-pending > span { display:flex; align-items:center; min-width:0; max-width:100%; gap:.25rem; padding:.1875rem .25rem .1875rem .4375rem; border:1px solid var(--board-border); border-radius:.375rem; color:var(--board-muted); font-size:.6875rem; line-height:1rem; }.board-log-pending > span > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-log-pending button { display:grid; place-items:center; flex:none; width:1.125rem; height:1.125rem; padding:0; border:0; border-radius:.25rem; background:transparent; color:var(--board-dim); cursor:pointer; }.board-log-pending button:hover { background:var(--board-hover); color:var(--board-text); }.board-log-pending .board-ui-icon { width:.6875rem; height:.6875rem; }
|
|
379
|
-
.board-log-empty { padding:.625rem .5rem; color:var(--board-dim); font-size:.75rem; text-align:center; }.board-log-list { display:flex; flex-direction:column; }.board-log-item { position:relative; margin-left:.25rem; padding:0
|
|
401
|
+
.board-log-empty { padding:.625rem .5rem; color:var(--board-dim); font-size:.75rem; text-align:center; }.board-log-list { display:flex; flex-direction:column; }.board-log-item { position:relative; margin-left:.25rem; padding:0 1.75rem .875rem 1rem; border-left:1px solid var(--board-border); }.board-log-item:last-child { padding-bottom:.125rem; }.board-log-item::before { position:absolute; top:.25rem; left:-.25rem; width:.4375rem; height:.4375rem; border:2px solid var(--board-surface); border-radius:50%; background:var(--dsw-alias-state-business-primary,#4176e6); content:''; }.board-log-item time { display:block; margin-bottom:.25rem; color:var(--board-dim); font-size:.625rem; line-height:.875rem; }.board-log-item p { margin:0; color:var(--board-text); font-size:.75rem; line-height:1.125rem; overflow-wrap:anywhere; white-space:pre-wrap; }.board-log-item p a { color:var(--dsw-alias-state-business-primary,#4176e6); text-decoration:none; }.board-log-item p a:hover { text-decoration:underline; }.board-log-delete { position:absolute; top:-.25rem; right:0; display:grid; place-items:center; width:1.5rem; height:1.5rem; padding:0; border:0; border-radius:.375rem; background:transparent; color:var(--board-dim); cursor:pointer; opacity:0; }.board-log-delete .board-ui-icon { width:.875rem; height:.875rem; }.board-log-item:hover .board-log-delete,.board-log-delete:focus-visible { opacity:.7; }.board-log-delete:hover { background:color-mix(in srgb,var(--board-danger) 10%,transparent); color:var(--board-danger); opacity:1; }
|
|
380
402
|
.board-log-attachments { display:flex; flex-wrap:wrap; gap:.375rem; margin-top:.4375rem; }.board-log-attachments button { display:flex; align-items:center; min-width:0; max-width:100%; gap:.375rem; min-height:1.75rem; padding:.25rem .5rem; border:1px solid var(--board-border); border-radius:.375rem; background:var(--board-raised); color:var(--board-muted); cursor:pointer; }.board-log-attachments button:hover { border-color:var(--board-border-strong); background:var(--board-hover); color:var(--board-text); }.board-log-attachments span { overflow:hidden; font-size:.6875rem; text-overflow:ellipsis; white-space:nowrap; }.board-log-attachments small { flex:none; color:var(--board-dim); font-size:.5625rem; }
|
|
381
403
|
.board-log-attachments .board-log-image { display:grid; grid-template-columns:minmax(0,1fr) auto; width:7.5rem; padding:.25rem; text-align:left; }.board-log-image img,.board-log-image-placeholder { grid-column:1 / -1; display:grid; place-items:center; width:100%; height:4.5rem; overflow:hidden; border-radius:.25rem; background:var(--board-subtle); color:var(--board-dim); object-fit:cover; font-size:.625rem; font-weight:700; }.board-log-image > span:not(.board-log-image-placeholder) { min-width:0; padding:.125rem .125rem 0; }.board-log-image > small { padding:.125rem .125rem 0; }.board-log-file::before { display:block; flex:none; width:.625rem; height:.75rem; border:1px solid currentColor; border-radius:.125rem; content:''; opacity:.65; }
|
|
382
404
|
.board-image-preview { position:fixed; z-index:320; inset:0; display:grid; place-items:center; padding:2rem; background:rgb(0 0 0 / 72%); }.board-image-preview-frame { position:relative; display:flex; flex-direction:column; align-items:center; max-width:min(90vw,75rem); max-height:90vh; gap:.5rem; }.board-image-preview-frame > img { display:block; max-width:100%; max-height:calc(90vh - 2.5rem); border-radius:.5rem; object-fit:contain; box-shadow:0 1rem 3rem rgb(0 0 0 / 35%); }.board-image-preview-frame > span { max-width:min(80vw,48rem); overflow:hidden; color:#fff; font-size:.75rem; text-overflow:ellipsis; white-space:nowrap; }.board-image-preview-frame > .board-iconbtn { position:absolute; z-index:2; top:.5rem; right:.5rem; border-radius:50%; background:rgb(0 0 0 / 55%); color:#fff; }.board-image-preview-frame > .board-iconbtn:hover { background:rgb(0 0 0 / 75%); color:#fff; }
|
|
@@ -464,10 +486,11 @@ body[data-ds-dark-theme] .board-global, body[data-ds-dark-theme] .board-overlay,
|
|
|
464
486
|
@media (prefers-reduced-motion:reduce) { .board-session-detail-layer { animation:none; } }
|
|
465
487
|
|
|
466
488
|
/* Desktop enhanced mode may wrap footer actions and shell overlays differently. */
|
|
467
|
-
:where(*):has(> [data-slot="sidebar.footer.action"]) { flex-direction:column !important; align-items:stretch !important; }
|
|
468
|
-
.board-side { flex:0 0 auto; align-self:stretch; width:
|
|
469
|
-
.board-side.rail { align-self:center; }
|
|
470
|
-
.board-global { position:fixed; z-index:10000; inset:0 0 0 auto; }
|
|
489
|
+
:where(*):has(> [data-slot="sidebar.footer.action"]) { width:100% !important; flex-direction:column !important; align-items:stretch !important; }
|
|
490
|
+
.board-side { flex:0 0 auto; align-self:stretch; width:100%; }
|
|
491
|
+
.board-side.rail { align-self:stretch; justify-content:center; width:100%; padding:0; }
|
|
492
|
+
.board-global { --board-titlebar-height:env(titlebar-area-height,0px); --board-titlebar-right:max(0px,calc(100vw - env(titlebar-area-x,0px) - env(titlebar-area-width,100vw))); position:fixed; z-index:10000; inset:0 0 0 auto; top:var(--board-shell-top,0px); }
|
|
493
|
+
.board-global::after { position:absolute; z-index:90; top:0; right:0; width:var(--board-titlebar-right); height:var(--board-titlebar-height); background:var(--board-bg); content:''; pointer-events:none; }
|
|
471
494
|
body:has([role="dialog"]:not([class^="board-"]):not([class*=" board-"])) .board-global { visibility:hidden; pointer-events:none; }
|
|
472
495
|
.board-session-detail-layer { z-index:10100; }
|
|
473
496
|
.board-date-popover-portal,.board-select-portal { z-index:10200; }
|
|
@@ -477,7 +500,9 @@ body:has([role="dialog"]:not([class^="board-"]):not([class*=" board-"])) .board-
|
|
|
477
500
|
|
|
478
501
|
/* Board-level tags. */
|
|
479
502
|
.board-composer-tag-trigger { margin-right:auto; color:var(--board-muted); }.board-composer-tag-trigger .board-history-icon { width:.875rem; height:.875rem; }.board-composer-tag-trigger b { display:grid; place-items:center; min-width:1.125rem; height:1.125rem; padding:0 .1875rem; border-radius:.5625rem; background:var(--board-active); color:var(--board-text); font-size:.625rem; line-height:1; }.board-composer-tag-menu { display:flex; flex-direction:column; gap:.1875rem; padding:.375rem; }.board-composer-tag-menu .board-select-option { box-sizing:border-box; min-height:2.125rem; padding:.375rem .625rem; border:1px solid transparent; border-radius:.5rem; }.board-composer-tag-menu .board-select-option[data-color="red"] { --tag-color:var(--board-danger,#d84b4b); }.board-composer-tag-menu .board-select-option[data-color="orange"] { --tag-color:var(--board-warn,#d7791f); }.board-composer-tag-menu .board-select-option[data-color="blue"] { --tag-color:var(--dsw-alias-brand-primary,#5c8df6); }.board-composer-tag-menu .board-select-option[data-color="green"] { --tag-color:var(--dsw-alias-state-success-primary,#27835b); }.board-composer-tag-menu .board-select-option > i { flex:none; width:.5rem; height:.5rem; border-radius:50%; background:var(--tag-color); }.board-composer-tag-menu .board-select-option[aria-selected="true"] { border-color:color-mix(in srgb,var(--tag-color) 38%,var(--board-border)); background:color-mix(in srgb,var(--tag-color) 10%,var(--board-surface)); }.board-composer-tag-menu .board-select-option[aria-selected="true"]::after { display:grid; place-items:center; width:1rem; height:1rem; border-radius:50%; background:var(--tag-color); color:white; font-size:.6875rem; line-height:1; }
|
|
480
|
-
.board-fab-stack { position:
|
|
503
|
+
.board-fab-stack { position:fixed; z-index:100; right:1.125rem; bottom:1.125rem; display:flex; align-items:center; gap:.5rem; }.board-fab-stack .board-archive-fab { position:static; }.board-tag-fab { display:grid; place-items:center; box-sizing:border-box; width:2.25rem; min-width:2.25rem; height:2.25rem; min-height:2.25rem; padding:0; border:1px solid var(--board-border); border-radius:50%; background:var(--board-raised); color:var(--board-muted); box-shadow:0 .25rem .875rem rgb(0 0 0 / 12%); }.board-tag-fab:hover { border-color:var(--board-border-strong); background:var(--board-hover); color:var(--board-text); }
|
|
504
|
+
.board-timeline-layer { position:absolute; z-index:70; inset:0; display:flex; min-width:0; min-height:0; flex-direction:column; overflow:hidden; background:var(--board-bg); color:var(--board-text); }.board-timeline-layer > header { display:flex; flex:none; align-items:center; min-height:3rem; gap:.5rem; padding:.5rem .75rem; border-bottom:1px solid var(--board-border); background:var(--board-surface); }.board-timeline-layer > header strong { font-size:.875rem; line-height:1.25rem; }.board-timeline-layer > header > span { padding:.125rem .4375rem; border-radius:.625rem; background:var(--board-active); color:var(--board-dim); font-size:.625rem; line-height:.875rem; }.board-timeline-layer > header strong + span { margin-left:auto; }.board-timeline-scroll { flex:1; min-width:0; min-height:0; overflow:auto; overscroll-behavior:contain; padding:0 1rem 1rem; }.board-timeline-canvas { position:relative; min-width:100%; min-height:100%; padding-bottom:6rem; }.board-timeline-axis { position:sticky; z-index:4; top:0; height:2.75rem; border-bottom:1px solid var(--board-border-strong); background:color-mix(in srgb,var(--board-bg) 92%,transparent); backdrop-filter:blur(.375rem); }.board-timeline-axis > span { position:absolute; bottom:.5rem; transform:translateX(-50%); color:var(--board-dim); font-size:.625rem; line-height:.875rem; white-space:nowrap; }.board-timeline-axis > span:first-child { transform:none; }.board-timeline-axis > span:last-child { transform:translateX(-100%); }.board-timeline-rows { padding-top:.25rem; }.board-timeline-row { position:relative; height:4rem; border-bottom:1px solid color-mix(in srgb,var(--board-border) 55%,transparent); }.board-timeline-row > i { position:absolute; top:0; bottom:0; width:1px; background:color-mix(in srgb,var(--board-border) 55%,transparent); pointer-events:none; }.board-timeline-task { --timeline-color:var(--dsw-alias-brand-primary,#5c8df6); position:absolute; z-index:1; top:.5625rem; display:flex; min-width:2rem; max-width:calc(100% - .25rem); height:2.875rem; flex-direction:column; justify-content:center; gap:.125rem; padding:.3125rem .5rem; overflow:visible; border:1px solid color-mix(in srgb,var(--timeline-color) 50%,var(--board-border)); border-radius:.5rem; outline:0; background:color-mix(in srgb,var(--timeline-color) 13%,var(--board-raised)); color:var(--board-text); cursor:pointer; font:inherit; text-align:left; box-shadow:0 .125rem .375rem rgb(0 0 0 / 8%); }.board-timeline-task[data-color="red"] { --timeline-color:var(--board-danger,#d84b4b); }.board-timeline-task[data-color="orange"] { --timeline-color:var(--board-warn,#d7791f); }.board-timeline-task[data-color="green"] { --timeline-color:var(--dsw-alias-state-success-primary,#27835b); }.board-timeline-task:hover,.board-timeline-task:focus-visible { z-index:12; border-color:var(--timeline-color); background:color-mix(in srgb,var(--timeline-color) 20%,var(--board-raised)); box-shadow:0 .375rem 1rem rgb(0 0 0 / 16%); }.board-timeline-task.milestone { width:2.875rem; min-width:2.875rem; padding:.3125rem; transform:translateX(-50%); border-radius:1.4375rem; }.board-timeline-task.milestone::before { position:absolute; top:50%; left:50%; width:.5rem; height:.5rem; transform:translate(-50%,-50%); border-radius:50%; background:var(--timeline-color); content:''; }.board-timeline-task.milestone > strong,.board-timeline-task.milestone > .board-timeline-bounds { display:none; }.board-timeline-task > strong { min-width:0; overflow:hidden; color:var(--board-text); font-size:.6875rem; line-height:1rem; text-overflow:ellipsis; white-space:nowrap; }.board-timeline-bounds { display:flex; min-width:0; align-items:center; justify-content:space-between; gap:.375rem; overflow:hidden; color:var(--board-dim); font-size:.5625rem; line-height:.8125rem; }.board-timeline-bounds > span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-timeline-tooltip { position:absolute; z-index:30; top:calc(100% + .4375rem); left:50%; display:none; width:min(18rem,calc(100vw - 3rem)); max-width:18rem; transform:translateX(-50%); padding:.625rem .75rem; border:1px solid var(--board-border-strong); border-radius:.625rem; background:var(--board-surface); color:var(--board-muted); box-shadow:var(--dsw-shadow-lv3,0 .75rem 2rem rgb(0 0 0 / 22%)); pointer-events:none; font-size:.6875rem; line-height:1rem; white-space:normal; }.board-timeline-task.edge-start .board-timeline-tooltip { right:auto; left:0; transform:none; }.board-timeline-task.edge-end .board-timeline-tooltip { right:0; left:auto; transform:none; }.board-timeline-task:hover .board-timeline-tooltip,.board-timeline-task:focus-visible .board-timeline-tooltip { display:flex; flex-direction:column; gap:.1875rem; }.board-timeline-tooltip > strong { color:var(--board-text); font-size:.75rem; line-height:1.125rem; }.board-timeline-note { display:-webkit-box; margin-top:.25rem; overflow:hidden; color:var(--board-dim); white-space:pre-wrap; -webkit-box-orient:vertical; -webkit-line-clamp:4; }.board-timeline-empty { display:flex; flex:1; min-height:0; flex-direction:column; align-items:center; justify-content:center; gap:.5rem; padding:2rem; color:var(--board-dim); text-align:center; }.board-timeline-empty > .board-history-icon { width:2.5rem; height:2.5rem; padding:.625rem; border-radius:50%; background:var(--board-active); color:var(--board-muted); }.board-timeline-empty > strong { color:var(--board-text); font-size:.875rem; }.board-timeline-empty > span { max-width:24rem; font-size:.75rem; line-height:1.125rem; }
|
|
505
|
+
.board-timeline-layer > header { flex-wrap:wrap; }.board-timeline-controls { display:flex; flex:none; align-items:center; gap:.125rem; margin-left:.25rem; padding:.125rem; border:1px solid var(--board-border); border-radius:.5rem; background:var(--board-raised); }.board-timeline-controls > .board-iconbtn { width:1.625rem; min-height:1.625rem; border-radius:.375rem; font-size:.875rem; }.board-timeline-controls > span { min-width:2.5rem; color:var(--board-dim); font-size:.5625rem; line-height:.8125rem; text-align:center; font-variant-numeric:tabular-nums; }.board-timeline-controls .board-timeline-fit { font-size:.75rem; }.board-timeline-row { height:2.875rem; }.board-timeline-task { top:.375rem; height:2.125rem; gap:0; padding:.1875rem .375rem; border-radius:.4375rem; }.board-timeline-task.milestone { width:2.125rem; min-width:2.125rem; height:2.125rem; padding:.1875rem; border-radius:1.0625rem; }.board-timeline-task > strong { font-size:.625rem; line-height:.875rem; }.board-timeline-bounds { font-size:.5rem; line-height:.6875rem; }.board-timeline-now { position:absolute; z-index:5; top:2.75rem; bottom:6rem; width:1px; border-left:1px solid var(--board-danger,#d84b4b); pointer-events:none; }.board-timeline-now::after { position:absolute; top:0; left:-.1875rem; width:.375rem; height:.375rem; border-radius:50%; background:var(--board-danger,#d84b4b); content:''; }.board-timeline-now > span { position:absolute; top:-1.375rem; left:0; transform:translateX(-50%); padding:.0625rem .25rem; border-radius:.25rem; background:var(--board-danger,#d84b4b); color:#fff; font-size:.5rem; line-height:.75rem; white-space:nowrap; }
|
|
481
506
|
.board-card-tags { display:flex; min-width:0; align-items:center; gap:.1875rem; margin-top:.25rem; overflow:hidden; }.board-tag,.board-tag-more { display:inline-flex; max-width:5.5rem; min-height:1rem; align-items:center; padding:0 .3125rem; overflow:hidden; border-radius:.25rem; font-size:.625rem; font-weight:650; line-height:1rem; text-overflow:ellipsis; white-space:nowrap; }.board-tag-more { flex:none; background:var(--board-active); color:var(--board-muted); }
|
|
482
507
|
.board-tag[data-color="red"],.board-tag-choice[data-color="red"] { --tag-color:var(--board-danger,#d84b4b); }.board-tag[data-color="orange"],.board-tag-choice[data-color="orange"] { --tag-color:var(--board-warn,#d7791f); }.board-tag[data-color="blue"],.board-tag-choice[data-color="blue"] { --tag-color:var(--dsw-alias-brand-primary,#5c8df6); }.board-tag[data-color="green"],.board-tag-choice[data-color="green"] { --tag-color:var(--dsw-alias-state-success-primary,#27835b); }.board-tag { background:color-mix(in srgb,var(--tag-color) 14%,transparent); color:var(--tag-color); }
|
|
483
508
|
.board-card-tag-picker { display:flex; flex-direction:column; gap:.375rem; margin-top:.75rem; padding:.625rem 0; border-top:1px solid var(--board-border); border-bottom:1px solid var(--board-border); }.board-card-tag-picker > strong { color:var(--board-muted); font-size:.6875rem; line-height:1rem; }.board-card-tag-picker > span { color:var(--board-dim); font-size:.6875rem; }.board-card-tag-picker > div { display:flex; flex-wrap:wrap; gap:.375rem; }.board-tag-choice { display:inline-flex; align-items:center; gap:.3125rem; min-height:1.75rem; padding:0 .5rem; border:1px solid var(--board-border); border-radius:.4375rem; background:var(--board-raised); color:var(--board-muted); cursor:pointer; font:inherit; font-size:.6875rem; }.board-tag-choice > span { width:.4375rem; height:.4375rem; border-radius:50%; background:var(--tag-color); }.board-tag-choice:hover { border-color:var(--tag-color); color:var(--board-text); }.board-tag-choice.selected { border-color:color-mix(in srgb,var(--tag-color) 55%,var(--board-border)); background:color-mix(in srgb,var(--tag-color) 12%,var(--board-raised)); color:var(--tag-color); font-weight:650; }
|
|
@@ -489,6 +514,11 @@ body[data-ds-dark-theme] .board-settings { --board-text:var(--dsw-alias-label-pr
|
|
|
489
514
|
.board-settings-row { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:.75rem 0; border-bottom:1px solid var(--board-border); }.board-settings-row > div { display:flex; min-width:0; flex-direction:column; gap:.125rem; }.board-settings-row strong,.board-settings-prompt strong { font-size:.75rem; line-height:1.125rem; }
|
|
490
515
|
.board-switch { position:relative; flex:none; width:2rem; height:1.125rem; padding:0; border:0; border-radius:1rem; background:var(--board-border); cursor:pointer; transition:background .15s ease; }.board-switch > span { position:absolute; top:.125rem; left:.125rem; width:.875rem; height:.875rem; border-radius:50%; background:var(--board-raised); box-shadow:0 1px 3px rgb(0 0 0 / 22%); transition:transform .15s ease; }.board-switch[aria-checked="true"] { background:var(--dsw-alias-brand-primary,#5c8df6); }.board-switch[aria-checked="true"] > span { transform:translateX(.875rem); }.board-switch:disabled { cursor:not-allowed; opacity:.45; }
|
|
491
516
|
.board-settings-prompt { display:flex; flex-direction:column; gap:.25rem; padding-top:.75rem; }.board-settings-prompt textarea { box-sizing:border-box; width:100%; min-height:6rem; max-height:12rem; padding:.5rem .625rem; resize:vertical; border:1px solid var(--board-border); border-radius:.5rem; outline:0; background:var(--board-raised); color:var(--board-text); font:inherit; font-size:.75rem; line-height:1.125rem; }.board-settings-prompt textarea:focus { border-color:var(--dsw-alias-brand-primary,#5c8df6); box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 18%,transparent); }.board-settings-prompt textarea::placeholder { color:var(--board-dim); }.board-settings-prompt small { align-self:flex-end; color:var(--board-dim); font-size:.625rem; line-height:.875rem; }
|
|
517
|
+
.board-session-detail-layer { --board-titlebar-right:max(0px,calc(100vw - env(titlebar-area-x,0px) - env(titlebar-area-width,100vw))); top:var(--board-shell-top,0px); }
|
|
518
|
+
.board-session-detail-drawer { position:relative; }
|
|
519
|
+
.board-session-detail-layer .board-detail-header { padding-right:var(--board-titlebar-right); }
|
|
520
|
+
.board-session-detail-layer .board-detail-header .board-iconbtn { right:calc(var(--board-titlebar-right) + .625rem); }
|
|
521
|
+
.board-session-detail-layer .board-detail-header .board-detail-back { position:static; width:1.875rem; height:1.875rem; min-height:1.875rem; }
|
|
492
522
|
`;
|
|
493
523
|
function ensureBoardStyles() {
|
|
494
524
|
if (typeof document === "undefined") return;
|
|
@@ -697,6 +727,10 @@ async function remoteOpenAttachment(ctx, sessionId, scope, cardId, attachmentId)
|
|
|
697
727
|
const result = await ctx.connection.rpc.call(CHANNEL, "openAttachment", { args: { sessionId, scope, cardId, attachmentId } });
|
|
698
728
|
if (!result.ok) throw new Error(result.error?.message ?? "attachment open failed");
|
|
699
729
|
}
|
|
730
|
+
async function remoteOpenUrl(ctx, sessionId, scope, url) {
|
|
731
|
+
const result = await ctx.connection.rpc.call(CHANNEL, "openUrl", { args: { sessionId, scope, url } });
|
|
732
|
+
if (!result.ok) throw new Error(result.error?.message ?? "URL open failed");
|
|
733
|
+
}
|
|
700
734
|
function normalizeBoard(board) {
|
|
701
735
|
return {
|
|
702
736
|
...board,
|
|
@@ -791,7 +825,8 @@ function useBoard(ctx, sessionId, scope, t) {
|
|
|
791
825
|
}, [acceptBoard, ctx, scope, sessionId]);
|
|
792
826
|
const downloadAttachment = (0, import_react.useCallback)((cardId, attachmentId) => remoteGetAttachment(ctx, sessionId, scope, cardId, attachmentId), [ctx, scope, sessionId]);
|
|
793
827
|
const openAttachment = (0, import_react.useCallback)((cardId, attachmentId) => remoteOpenAttachment(ctx, sessionId, scope, cardId, attachmentId), [ctx, scope, sessionId]);
|
|
794
|
-
|
|
828
|
+
const openUrl = (0, import_react.useCallback)((url) => remoteOpenUrl(ctx, sessionId, scope, url), [ctx, scope, sessionId]);
|
|
829
|
+
return { board, loading, error, conflict, mutate, addLog, downloadAttachment, openAttachment, openUrl, reload };
|
|
795
830
|
}
|
|
796
831
|
|
|
797
832
|
// src/client/settings.ts
|
|
@@ -924,6 +959,7 @@ function BoardEditor(props) {
|
|
|
924
959
|
const [selectedId, setSelectedId] = (0, import_react3.useState)(null);
|
|
925
960
|
const [archiveOpen, setArchiveOpen] = (0, import_react3.useState)(false);
|
|
926
961
|
const [tagManagerOpen, setTagManagerOpen] = (0, import_react3.useState)(false);
|
|
962
|
+
const [timelineOpen, setTimelineOpen] = (0, import_react3.useState)(false);
|
|
927
963
|
const [draggedCardId, setDraggedCardId] = (0, import_react3.useState)(null);
|
|
928
964
|
const [draggedColumnId, setDraggedColumnId] = (0, import_react3.useState)(null);
|
|
929
965
|
const [cardPreview, setCardPreview] = (0, import_react3.useState)(null);
|
|
@@ -1192,14 +1228,16 @@ function BoardEditor(props) {
|
|
|
1192
1228
|
return onMutate({ type: "addColumn", title });
|
|
1193
1229
|
} })
|
|
1194
1230
|
] }) }),
|
|
1195
|
-
selected !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetailLayer, { card: selected, tags: board.tags, t, onClose: () => setSelectedId(null), onSave: (patch) => onMutate({ type: "updateCard", cardId: selected.id, ...patch }), onSetTags: (tagIds) => onMutate({ type: "setCardTags", cardId: selected.id, tagIds }), onAddLog: (text, attachments) => props.onAddLog(selected.id, text, attachments), onDownloadAttachment: (attachmentId) => props.onDownloadAttachment(selected.id, attachmentId), onOpenAttachment: (attachmentId) => props.onOpenAttachment(selected.id, attachmentId) }, selected.id),
|
|
1231
|
+
selected !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetailLayer, { card: selected, tags: board.tags, t, onClose: () => setSelectedId(null), onSave: (patch) => onMutate({ type: "updateCard", cardId: selected.id, ...patch }), onSetTags: (tagIds) => onMutate({ type: "setCardTags", cardId: selected.id, tagIds }), onAddLog: (text, attachments) => props.onAddLog(selected.id, text, attachments), onDeleteLog: (logId) => onMutate({ type: "deleteLog", cardId: selected.id, logId }), onDownloadAttachment: (attachmentId) => props.onDownloadAttachment(selected.id, attachmentId), onOpenAttachment: (attachmentId) => props.onOpenAttachment(selected.id, attachmentId), onOpenUrl: props.onOpenUrl }, selected.id),
|
|
1196
1232
|
!singleColumn && archiveOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArchivePanelLayer, { cards: archivedCards, columns: baseColumns, tags: board.tags, t, onClose: () => setArchiveOpen(false), onRestore: (cardId, columnId) => onMutate({ type: "restoreCard", cardId, columnId }), onRestoreMany: (cards) => onMutate({ type: "restoreCards", cards }), onRequestDelete: (cards) => {
|
|
1197
1233
|
if (cards.length === 1 && cards[0] !== void 0) setConfirmTarget({ kind: "card", id: cards[0].id, title: cards[0].title });
|
|
1198
1234
|
else if (cards.length > 1) setConfirmTarget({ kind: "cards", ids: cards.map((card) => card.id), count: cards.length });
|
|
1199
1235
|
} }),
|
|
1200
|
-
!singleColumn && tagManagerOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TagManagerLayer, { tags: board.tags, t, onClose: () => setTagManagerOpen(false), onMutate })
|
|
1236
|
+
!singleColumn && tagManagerOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TagManagerLayer, { tags: board.tags, t, onClose: () => setTagManagerOpen(false), onMutate }),
|
|
1237
|
+
!singleColumn && timelineOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimelineView, { cards: board.cards, columns: baseColumns, tags: board.tags, t, onClose: () => setTimelineOpen(false), onOpenCard: setSelectedId })
|
|
1201
1238
|
] }),
|
|
1202
|
-
!singleColumn && !archiveOpen && !tagManagerOpen && selected === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-fab-stack", children: [
|
|
1239
|
+
!singleColumn && !archiveOpen && !tagManagerOpen && !timelineOpen && selected === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-fab-stack", children: [
|
|
1240
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn board-tag-fab", title: t("timelineView"), "aria-label": t("timelineView"), onClick: () => setTimelineOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimelineIcon, {}) }),
|
|
1203
1241
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn board-tag-fab", title: t("manageTags"), "aria-label": t("manageTags"), onClick: () => setTagManagerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TagIcon, {}) }),
|
|
1204
1242
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn board-open-archive board-archive-fab", title: t("completedCount", { n: archivedCards.length }), "aria-label": t("completedCount", { n: archivedCards.length }), onClick: () => setArchiveOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HistoryIcon, {}) })
|
|
1205
1243
|
] }),
|
|
@@ -1272,6 +1310,14 @@ function TagIcon() {
|
|
|
1272
1310
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "7", cy: "7", r: "1" })
|
|
1273
1311
|
] });
|
|
1274
1312
|
}
|
|
1313
|
+
function TimelineIcon() {
|
|
1314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { className: "board-history-icon", viewBox: "0 0 20 20", "aria-hidden": "true", children: [
|
|
1315
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M3.5 5.5h13M3.5 10h13M3.5 14.5h13" }),
|
|
1316
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "7", cy: "5.5", r: "1.5" }),
|
|
1317
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "13", cy: "10", r: "1.5" }),
|
|
1318
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "9", cy: "14.5", r: "1.5" })
|
|
1319
|
+
] });
|
|
1320
|
+
}
|
|
1275
1321
|
function CardTags(props) {
|
|
1276
1322
|
const tags = props.card.tagIds.flatMap((id) => {
|
|
1277
1323
|
const tag = props.tags.find((item) => item.id === id);
|
|
@@ -1589,7 +1635,7 @@ function CardDetailLayer(props) {
|
|
|
1589
1635
|
commitRef.current();
|
|
1590
1636
|
props.onClose();
|
|
1591
1637
|
};
|
|
1592
|
-
return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-session-detail-layer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: drawer, className: "board-session-detail-drawer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetail, { card: props.card, tags: props.tags, t: props.t, commitRef, onClose: closeDetail, onSave: props.onSave, onSetTags: props.onSetTags, onAddLog: props.onAddLog, onDownloadAttachment: props.onDownloadAttachment, onOpenAttachment: props.onOpenAttachment }) }) }), document.body);
|
|
1638
|
+
return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-session-detail-layer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: drawer, className: "board-session-detail-drawer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetail, { card: props.card, tags: props.tags, t: props.t, commitRef, onClose: closeDetail, onSave: props.onSave, onSetTags: props.onSetTags, onAddLog: props.onAddLog, onDeleteLog: props.onDeleteLog, onDownloadAttachment: props.onDownloadAttachment, onOpenAttachment: props.onOpenAttachment, onOpenUrl: props.onOpenUrl }) }) }), document.body);
|
|
1593
1639
|
}
|
|
1594
1640
|
function CardDetail(props) {
|
|
1595
1641
|
const titleRef = (0, import_react3.useRef)(null);
|
|
@@ -1632,8 +1678,8 @@ function CardDetail(props) {
|
|
|
1632
1678
|
}
|
|
1633
1679
|
}, children: [
|
|
1634
1680
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-header", children: [
|
|
1635
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("
|
|
1636
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("
|
|
1681
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn board-detail-back", "aria-label": props.t("back"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "left" }) }),
|
|
1682
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-detail-heading", children: props.t("cardDetails") })
|
|
1637
1683
|
] }),
|
|
1638
1684
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-editor", children: [
|
|
1639
1685
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-main", children: [
|
|
@@ -1667,7 +1713,7 @@ function CardDetail(props) {
|
|
|
1667
1713
|
] }) })
|
|
1668
1714
|
] }),
|
|
1669
1715
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardTagPicker, { card: props.card, tags: props.tags, t: props.t, onChange: props.onSetTags }),
|
|
1670
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardLogSection, { card: props.card, t: props.t, onAdd: props.onAddLog, onDownload: props.onDownloadAttachment, onOpen: props.onOpenAttachment })
|
|
1716
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardLogSection, { card: props.card, t: props.t, onAdd: props.onAddLog, onDelete: props.onDeleteLog, onDownload: props.onDownloadAttachment, onOpen: props.onOpenAttachment, onOpenUrl: props.onOpenUrl })
|
|
1671
1717
|
] });
|
|
1672
1718
|
}
|
|
1673
1719
|
function CardTagPicker(props) {
|
|
@@ -1741,6 +1787,22 @@ function CardLogSection(props) {
|
|
|
1741
1787
|
setError(readableLogError(reason, props.t));
|
|
1742
1788
|
}
|
|
1743
1789
|
};
|
|
1790
|
+
const openUrl = async (url) => {
|
|
1791
|
+
try {
|
|
1792
|
+
setError(null);
|
|
1793
|
+
await props.onOpenUrl(url);
|
|
1794
|
+
} catch (reason) {
|
|
1795
|
+
setError(readableLogError(reason, props.t));
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
const deleteLog = async (logId) => {
|
|
1799
|
+
try {
|
|
1800
|
+
setError(null);
|
|
1801
|
+
await props.onDelete(logId);
|
|
1802
|
+
} catch (reason) {
|
|
1803
|
+
setError(readableLogError(reason, props.t));
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1744
1806
|
const logs = [...props.card.logs].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
1745
1807
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "board-log-section", "aria-label": props.t("logs"), children: [
|
|
1746
1808
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-log-heading", children: [
|
|
@@ -1749,7 +1811,8 @@ function CardLogSection(props) {
|
|
|
1749
1811
|
] }),
|
|
1750
1812
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-scroll", children: logs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-empty", children: props.t("noLogs") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-list", children: logs.map((log) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: "board-log-item", children: [
|
|
1751
1813
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: log.createdAt, children: formatLogDate(log.createdAt) }),
|
|
1752
|
-
|
|
1814
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-log-delete", title: props.t("deleteLog"), "aria-label": props.t("deleteLog"), onClick: () => void deleteLog(log.id), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrashIcon, {}) }),
|
|
1815
|
+
log.text.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LogText, { text: log.text, onOpen: openUrl }),
|
|
1753
1816
|
log.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-attachments", children: log.attachments.map((attachment) => isImageAttachment(attachment) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-log-image", title: props.t("viewImage", { name: attachment.name }), onClick: () => void openImage(attachment), children: [
|
|
1754
1817
|
attachment.preview !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: attachment.preview, alt: "" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-log-image-placeholder", children: props.t("imagePlaceholder") }),
|
|
1755
1818
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: attachment.name }),
|
|
@@ -1789,6 +1852,24 @@ function CardLogSection(props) {
|
|
|
1789
1852
|
imagePreview !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ImagePreviewLayer, { preview: imagePreview, t: props.t, onClose: () => setImagePreview(null) })
|
|
1790
1853
|
] });
|
|
1791
1854
|
}
|
|
1855
|
+
function LogText(props) {
|
|
1856
|
+
const parts = [];
|
|
1857
|
+
let cursor = 0;
|
|
1858
|
+
for (const match of props.text.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
|
|
1859
|
+
const start = match.index;
|
|
1860
|
+
const raw = match[0];
|
|
1861
|
+
const url = raw.replace(/[),.;!?,。;:!?)】]+$/u, "");
|
|
1862
|
+
parts.push(props.text.slice(cursor, start));
|
|
1863
|
+
parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: url, target: "_blank", rel: "noreferrer", onClick: (event) => {
|
|
1864
|
+
event.preventDefault();
|
|
1865
|
+
void props.onOpen(url);
|
|
1866
|
+
}, children: url }, `${String(start)}:${url}`));
|
|
1867
|
+
parts.push(raw.slice(url.length));
|
|
1868
|
+
cursor = start + raw.length;
|
|
1869
|
+
}
|
|
1870
|
+
parts.push(props.text.slice(cursor));
|
|
1871
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: parts });
|
|
1872
|
+
}
|
|
1792
1873
|
function ImagePreviewLayer(props) {
|
|
1793
1874
|
(0, import_react3.useEffect)(() => {
|
|
1794
1875
|
const close = (event) => {
|
|
@@ -1879,6 +1960,154 @@ function CardTimeProgress(props) {
|
|
|
1879
1960
|
] })
|
|
1880
1961
|
] });
|
|
1881
1962
|
}
|
|
1963
|
+
function TimelineView(props) {
|
|
1964
|
+
const scrollRef = (0, import_react3.useRef)(null);
|
|
1965
|
+
const [viewportWidth, setViewportWidth] = (0, import_react3.useState)(0);
|
|
1966
|
+
const [zoom, setZoom] = (0, import_react3.useState)(1);
|
|
1967
|
+
const [nowMs, setNowMs] = (0, import_react3.useState)(() => Date.now());
|
|
1968
|
+
(0, import_react3.useEffect)(() => {
|
|
1969
|
+
const close = (event) => {
|
|
1970
|
+
if (event.key !== "Escape" || document.querySelector(".board-session-detail-layer,.board-select-portal,.board-confirm-backdrop") !== null) return;
|
|
1971
|
+
event.preventDefault();
|
|
1972
|
+
event.stopImmediatePropagation();
|
|
1973
|
+
props.onClose();
|
|
1974
|
+
};
|
|
1975
|
+
document.addEventListener("keydown", close);
|
|
1976
|
+
return () => document.removeEventListener("keydown", close);
|
|
1977
|
+
}, [props.onClose]);
|
|
1978
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1979
|
+
const scroll = scrollRef.current;
|
|
1980
|
+
if (scroll === null) return;
|
|
1981
|
+
const update = () => setViewportWidth(scroll.clientWidth);
|
|
1982
|
+
update();
|
|
1983
|
+
const observer = new ResizeObserver(update);
|
|
1984
|
+
observer.observe(scroll);
|
|
1985
|
+
return () => observer.disconnect();
|
|
1986
|
+
}, [props.cards]);
|
|
1987
|
+
(0, import_react3.useEffect)(() => {
|
|
1988
|
+
const timer = window.setInterval(() => setNowMs(Date.now()), 6e4);
|
|
1989
|
+
return () => window.clearInterval(timer);
|
|
1990
|
+
}, []);
|
|
1991
|
+
const active = props.cards.filter((card) => card.archivedAt === null);
|
|
1992
|
+
const cards = active.filter(hasTimelineTime).sort((a, b) => timelineTime(a) - timelineTime(b));
|
|
1993
|
+
const unscheduled = active.length - cards.length;
|
|
1994
|
+
const columnById = new Map(props.columns.map((column) => [column.id, column]));
|
|
1995
|
+
const tagById = new Map(props.tags.map((tag) => [tag.id, tag]));
|
|
1996
|
+
const values = [...cards.flatMap((card) => [card.startAt, card.endAt].flatMap((value) => value === null ? [] : [Date.parse(value)])), nowMs];
|
|
1997
|
+
const day = 864e5;
|
|
1998
|
+
const first = Math.min(...values);
|
|
1999
|
+
const last = Math.max(...values);
|
|
2000
|
+
const padding = Math.min(day * 7, Math.max(day / 4, (last - first) * 0.04));
|
|
2001
|
+
let rangeStart = first - padding;
|
|
2002
|
+
let rangeEnd = last + padding;
|
|
2003
|
+
if (rangeEnd - rangeStart < day) {
|
|
2004
|
+
const middle = (rangeStart + rangeEnd) / 2;
|
|
2005
|
+
rangeStart = middle - day / 2;
|
|
2006
|
+
rangeEnd = middle + day / 2;
|
|
2007
|
+
}
|
|
2008
|
+
const span = rangeEnd - rangeStart;
|
|
2009
|
+
const canvasWidth = Math.max(320, (viewportWidth || 992) - 32) * zoom;
|
|
2010
|
+
const tickCount = Math.max(6, Math.min(24, Math.round(canvasWidth / 170)));
|
|
2011
|
+
const ticks = Array.from({ length: tickCount }, (_, index) => ({ at: rangeStart + span * index / (tickCount - 1), left: index * 100 / (tickCount - 1) }));
|
|
2012
|
+
const nowLeft = (nowMs - rangeStart) / span * 100;
|
|
2013
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
2014
|
+
const scroll = scrollRef.current;
|
|
2015
|
+
if (scroll !== null && zoom > 1) scroll.scrollLeft = Math.max(0, nowLeft / 100 * canvasWidth - scroll.clientWidth / 2);
|
|
2016
|
+
}, [zoom]);
|
|
2017
|
+
if (cards.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "board-timeline-layer", "aria-label": props.t("timelineTitle"), children: [
|
|
2018
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { children: [
|
|
2019
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn board-detail-back", "aria-label": props.t("back"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "left" }) }),
|
|
2020
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("timelineTitle") })
|
|
2021
|
+
] }),
|
|
2022
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-timeline-empty", children: [
|
|
2023
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimelineIcon, {}),
|
|
2024
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("timelineEmpty") }),
|
|
2025
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("timelineEmptyHint") })
|
|
2026
|
+
] })
|
|
2027
|
+
] });
|
|
2028
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "board-timeline-layer", "aria-label": props.t("timelineTitle"), children: [
|
|
2029
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { children: [
|
|
2030
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn board-detail-back", "aria-label": props.t("back"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "left" }) }),
|
|
2031
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("timelineTitle") }),
|
|
2032
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("timelineTaskCount", { n: cards.length }) }),
|
|
2033
|
+
unscheduled > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("timelineUnscheduled", { n: unscheduled }) }),
|
|
2034
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-timeline-controls", children: [
|
|
2035
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", title: props.t("timelineZoomOut"), "aria-label": props.t("timelineZoomOut"), disabled: zoom <= 1, onClick: () => setZoom((value) => Math.max(1, value - 0.5)), children: "\u2212" }),
|
|
2036
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2037
|
+
Math.round(zoom * 100),
|
|
2038
|
+
"%"
|
|
2039
|
+
] }),
|
|
2040
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", title: props.t("timelineZoomIn"), "aria-label": props.t("timelineZoomIn"), disabled: zoom >= 4, onClick: () => setZoom((value) => Math.min(4, value + 0.5)), children: "+" }),
|
|
2041
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn board-timeline-fit", title: props.t("timelineFit"), "aria-label": props.t("timelineFit"), disabled: zoom === 1, onClick: () => setZoom(1), children: "\u2194" })
|
|
2042
|
+
] })
|
|
2043
|
+
] }),
|
|
2044
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: scrollRef, className: "board-timeline-scroll", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-timeline-canvas", style: { width: `${String(canvasWidth)}px` }, children: [
|
|
2045
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-timeline-axis", children: ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { left: `${String(tick.left)}%` }, children: formatTimelineDate(tick.at, span) }, tick.at)) }),
|
|
2046
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-timeline-now", style: { left: `${String(nowLeft)}%` }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("timelineNow") }) }),
|
|
2047
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-timeline-rows", children: cards.map((card) => {
|
|
2048
|
+
const start = card.startAt === null ? null : Date.parse(card.startAt);
|
|
2049
|
+
const end = card.endAt === null ? null : Date.parse(card.endAt);
|
|
2050
|
+
const left = ((start ?? end ?? rangeStart) - rangeStart) / span * 100;
|
|
2051
|
+
const right = ((end ?? start ?? rangeStart) - rangeStart) / span * 100;
|
|
2052
|
+
const milestone = start === null || end === null;
|
|
2053
|
+
const tags = card.tagIds.flatMap((id) => {
|
|
2054
|
+
const tag = tagById.get(id);
|
|
2055
|
+
return tag === void 0 ? [] : [tag];
|
|
2056
|
+
});
|
|
2057
|
+
const anchor = milestone ? left : (left + right) / 2;
|
|
2058
|
+
const edge = anchor < 15 ? " edge-start" : anchor > 85 ? " edge-end" : "";
|
|
2059
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-timeline-row", children: [
|
|
2060
|
+
ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", { style: { left: `${String(tick.left)}%` } }, tick.at)),
|
|
2061
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: `board-timeline-task${milestone ? " milestone" : ""}${edge}`, "data-color": tags[0]?.color ?? "blue", style: { left: `${String(left)}%`, width: milestone ? void 0 : `${String(Math.max(0.35, right - left))}%` }, onClick: () => props.onOpenCard(card.id), children: [
|
|
2062
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: card.title }),
|
|
2063
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "board-timeline-bounds", children: [
|
|
2064
|
+
card.startAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2065
|
+
"\u25B6 ",
|
|
2066
|
+
formatTimelineShort(card.startAt)
|
|
2067
|
+
] }),
|
|
2068
|
+
card.endAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2069
|
+
"\u25A0 ",
|
|
2070
|
+
formatTimelineShort(card.endAt)
|
|
2071
|
+
] })
|
|
2072
|
+
] }),
|
|
2073
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "board-timeline-tooltip", role: "tooltip", children: [
|
|
2074
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: card.title }),
|
|
2075
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: columnById.get(card.columnId)?.title ?? "" }),
|
|
2076
|
+
card.startAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2077
|
+
props.t("startTime"),
|
|
2078
|
+
": ",
|
|
2079
|
+
formatCardDate(card.startAt)
|
|
2080
|
+
] }),
|
|
2081
|
+
card.endAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2082
|
+
props.t("endTime"),
|
|
2083
|
+
": ",
|
|
2084
|
+
formatCardDate(card.endAt)
|
|
2085
|
+
] }),
|
|
2086
|
+
tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
2087
|
+
props.t("tags"),
|
|
2088
|
+
": ",
|
|
2089
|
+
tags.map((tag) => tag.name).join(", ")
|
|
2090
|
+
] }),
|
|
2091
|
+
card.body.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-timeline-note", children: card.body })
|
|
2092
|
+
] })
|
|
2093
|
+
] })
|
|
2094
|
+
] }, card.id);
|
|
2095
|
+
}) })
|
|
2096
|
+
] }) })
|
|
2097
|
+
] });
|
|
2098
|
+
}
|
|
2099
|
+
function hasTimelineTime(card) {
|
|
2100
|
+
return card.startAt !== null && Number.isFinite(Date.parse(card.startAt)) || card.endAt !== null && Number.isFinite(Date.parse(card.endAt));
|
|
2101
|
+
}
|
|
2102
|
+
function timelineTime(card) {
|
|
2103
|
+
return Date.parse(card.startAt ?? card.endAt ?? "");
|
|
2104
|
+
}
|
|
2105
|
+
function formatTimelineDate(value, span) {
|
|
2106
|
+
return new Intl.DateTimeFormat(activeLocale(), span > 300 * 864e5 ? { year: "numeric", month: "short" } : { month: "short", day: "numeric" }).format(value);
|
|
2107
|
+
}
|
|
2108
|
+
function formatTimelineShort(value) {
|
|
2109
|
+
return new Intl.DateTimeFormat(activeLocale(), { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
|
|
2110
|
+
}
|
|
1882
2111
|
function ConfirmDialog(props) {
|
|
1883
2112
|
const confirmRef = (0, import_react3.useRef)(null);
|
|
1884
2113
|
(0, import_react3.useEffect)(() => {
|
|
@@ -2481,6 +2710,7 @@ function SessionOverlay(props) {
|
|
|
2481
2710
|
onAddLog: boardState.addLog,
|
|
2482
2711
|
onDownloadAttachment: boardState.downloadAttachment,
|
|
2483
2712
|
onOpenAttachment: boardState.openAttachment,
|
|
2713
|
+
onOpenUrl: boardState.openUrl,
|
|
2484
2714
|
onRetry: boardState.reload
|
|
2485
2715
|
}
|
|
2486
2716
|
) })
|
|
@@ -2492,14 +2722,37 @@ var import_react6 = require("react");
|
|
|
2492
2722
|
var import_react7 = require("react");
|
|
2493
2723
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
2494
2724
|
function SidebarAction(props) {
|
|
2725
|
+
const actionRef = (0, import_react6.useRef)(null);
|
|
2495
2726
|
(0, import_react6.useEffect)(() => {
|
|
2496
2727
|
setSidebarWide(props.wide);
|
|
2497
2728
|
}, [props.wide]);
|
|
2729
|
+
(0, import_react6.useLayoutEffect)(() => {
|
|
2730
|
+
const action = actionRef.current;
|
|
2731
|
+
if (action === null) return;
|
|
2732
|
+
let shell = null;
|
|
2733
|
+
for (let node = action.parentElement; node !== null && node !== document.body; node = node.parentElement) {
|
|
2734
|
+
if (node.getBoundingClientRect().height >= window.innerHeight * 0.7) {
|
|
2735
|
+
shell = node;
|
|
2736
|
+
break;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const update = () => document.documentElement.style.setProperty("--board-shell-top", `${String(Math.max(0, Math.round(shell?.getBoundingClientRect().top ?? 0)))}px`);
|
|
2740
|
+
update();
|
|
2741
|
+
const observer = new ResizeObserver(update);
|
|
2742
|
+
if (shell !== null) observer.observe(shell);
|
|
2743
|
+
window.addEventListener("resize", update);
|
|
2744
|
+
return () => {
|
|
2745
|
+
observer.disconnect();
|
|
2746
|
+
window.removeEventListener("resize", update);
|
|
2747
|
+
document.documentElement.style.removeProperty("--board-shell-top");
|
|
2748
|
+
};
|
|
2749
|
+
}, [props.wide]);
|
|
2498
2750
|
const panel = (0, import_react7.useSyncExternalStore)(subscribeGlobalPanel, getGlobalPanel);
|
|
2499
2751
|
const { board } = useBoard(props.rpc, props.sessionId, "global", props.t);
|
|
2500
2752
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
2501
2753
|
"button",
|
|
2502
2754
|
{
|
|
2755
|
+
ref: actionRef,
|
|
2503
2756
|
type: "button",
|
|
2504
2757
|
"data-board-sidebar-action": true,
|
|
2505
2758
|
className: props.wide ? "board-side" : "board-side rail",
|
|
@@ -2554,7 +2807,7 @@ function GlobalPanel(props) {
|
|
|
2554
2807
|
(0, import_react8.useEffect)(() => {
|
|
2555
2808
|
if (!panel.open) return;
|
|
2556
2809
|
const onKey = (event) => {
|
|
2557
|
-
if (event.key !== "Escape" || document.querySelector(".board-session-detail-layer,.board-select-portal,.board-date-popover-portal,.board-image-preview,.board-confirm-backdrop,.board-card-composer") !== null) return;
|
|
2810
|
+
if (event.key !== "Escape" || document.querySelector(".board-session-detail-layer,.board-select-portal,.board-date-popover-portal,.board-image-preview,.board-confirm-backdrop,.board-card-composer,.board-timeline-layer") !== null) return;
|
|
2558
2811
|
setGlobalPanelOpen(false);
|
|
2559
2812
|
};
|
|
2560
2813
|
window.addEventListener("keydown", onKey);
|
|
@@ -2595,6 +2848,7 @@ function GlobalPanel(props) {
|
|
|
2595
2848
|
onAddLog: boardState.addLog,
|
|
2596
2849
|
onDownloadAttachment: boardState.downloadAttachment,
|
|
2597
2850
|
onOpenAttachment: boardState.openAttachment,
|
|
2851
|
+
onOpenUrl: boardState.openUrl,
|
|
2598
2852
|
onRetry: boardState.reload
|
|
2599
2853
|
}
|
|
2600
2854
|
) }) }),
|
package/lib/domain.d.ts
CHANGED
package/lib/domain.js
CHANGED
|
@@ -329,6 +329,15 @@ export function applyMutate(board, op, now = new Date(), createId = newId) {
|
|
|
329
329
|
card.updatedAt = at;
|
|
330
330
|
break;
|
|
331
331
|
}
|
|
332
|
+
case 'deleteLog': {
|
|
333
|
+
const card = requireCard(next, op.cardId);
|
|
334
|
+
const index = card.logs.findIndex((log) => log.id === op.logId);
|
|
335
|
+
if (index < 0)
|
|
336
|
+
throw new BoardNotFoundError();
|
|
337
|
+
card.logs.splice(index, 1);
|
|
338
|
+
card.updatedAt = at;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
332
341
|
case 'moveCard': {
|
|
333
342
|
moveCard(next, op.cardId, op.columnId, op.index);
|
|
334
343
|
requireCard(next, op.cardId).updatedAt = at;
|
package/lib/prompt.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
export const BOARD_SYSTEM_PROMPT = [
|
|
3
3
|
'Board protocol (mandatory):',
|
|
4
4
|
'1. The board_* tools are the only supported interface for Board data. Never inspect or edit Board storage JSON, plugin source, installation files, or private HTTP endpoints to answer or perform a Board request. Do not use shell/file/code tools as a fallback. If board_* is unavailable or still fails after one corrected retry, report the limitation instead of bypassing the interface.',
|
|
5
|
-
'2. Choose exactly the shortest supported flow. Existing task named by the user: board_search(scope: global unless explicitly session-only) -> board_get only if its notes/details are needed -> board_apply. New task with a known columnId: board_apply directly. New task with only a column name: board_list -> board_apply. Overview: board_list. Never call board_list and board_search together without a distinct need.',
|
|
6
|
-
'3. Never guess IDs or use a title as an ID. columnId comes from board_list; cardId comes from board_list/board_search; tagId comes from board_list. Reuse IDs returned by board_apply. On failure, read the error, refresh at most once, correct the arguments, and never repeat the identical failed call.',
|
|
5
|
+
'2. Choose exactly the shortest supported flow. Existing task named by the user: board_search(scope: global unless explicitly session-only) -> board_get only if its notes/details or log IDs are needed -> board_apply. New task with a known columnId: board_apply directly. New task with only a column name: board_list -> board_apply. Overview: board_list. Never call board_list and board_search together without a distinct need.',
|
|
6
|
+
'3. Never guess IDs or use a title as an ID. columnId comes from board_list; cardId comes from board_list/board_search; tagId comes from board_list; logId comes from board_get. Reuse IDs returned by board_apply. On failure, read the error, refresh at most once, correct the arguments, and never repeat the identical failed call.',
|
|
7
7
|
'4. You have one unified Board. Global scope contains all normal and session-linked columns and is fully controllable from any conversation. Session scope contains only this conversation\'s single task column; use it only when the user explicitly means this conversation or the new task clearly belongs here.',
|
|
8
8
|
'5. If this session has no column and a task must be created here, board_apply addColumn once in session scope, then use the returned columnId for addCard. Never create a session column merely because a conversation was opened, and never create a second session column.',
|
|
9
|
-
'6. Operation map: addColumn creates a column; addCard creates a task; renameColumn/updateCard edit; addTag/updateTag/deleteTag manage reusable tags; setCardTags replaces a card\'s tags; addLog appends
|
|
10
|
-
'7. Completing means archiveCard, never deleteCard.
|
|
9
|
+
'6. Operation map: addColumn creates a column; addCard creates a task; renameColumn/updateCard edit; addTag/updateTag/deleteTag manage reusable tags; setCardTags replaces a card\'s tags; addLog appends a process note/blocker/decision; deleteLog permanently removes one log and its attachments; moveCard/reorderColumns organize; archiveCard completes; restoreCard/restoreCards recover. updateCard saves title, notes, startAt/endAt, and timeReminder together.',
|
|
10
|
+
'7. Completing means archiveCard, never deleteCard. deleteCard, deleteCards, and deleteLog are permanent and allowed only when the user explicitly asks to delete. Archived tasks remain readable through board_get and searchable through board_search(status: archived).',
|
|
11
11
|
'8. timeReminder notifies once at startAt and once at endAt. Either boundary may be null; when both exist startAt must be earlier than endAt.',
|
|
12
12
|
'9. todo_write is this turn\'s short-lived plan. Board is only for tasks the user wants to persist across turns. Fetch card notes with board_get when needed; do not mirror card data into this prompt.',
|
|
13
13
|
].join('\n');
|
package/lib/rpc.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ type BoardRpcTarget = {
|
|
|
8
8
|
openAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
9
9
|
opened: true;
|
|
10
10
|
}>;
|
|
11
|
+
openUrl(url: string): Promise<{
|
|
12
|
+
opened: true;
|
|
13
|
+
}>;
|
|
11
14
|
};
|
|
12
15
|
export declare const BOARD_RPC_CHANNEL = "/board-rpc";
|
|
13
16
|
/** Host Connection is optional: tools still work in assemblies without a web carrier. */
|
|
@@ -17,5 +20,8 @@ export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'get
|
|
|
17
20
|
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'openAttachment', payload: unknown): Promise<{
|
|
18
21
|
opened: true;
|
|
19
22
|
}>;
|
|
23
|
+
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'openUrl', payload: unknown): Promise<{
|
|
24
|
+
opened: true;
|
|
25
|
+
}>;
|
|
20
26
|
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: string, payload: unknown): Promise<unknown>;
|
|
21
27
|
export {};
|
package/lib/rpc.js
CHANGED
|
@@ -44,6 +44,9 @@ export async function dispatchBoardRpc(service, endpoint, payload) {
|
|
|
44
44
|
if (endpoint === 'openAttachment') {
|
|
45
45
|
return service.openAttachment(sessionId, scope, requireString(args, 'cardId'), requireString(args, 'attachmentId'));
|
|
46
46
|
}
|
|
47
|
+
if (endpoint === 'openUrl') {
|
|
48
|
+
return service.openUrl(requireString(args, 'url'));
|
|
49
|
+
}
|
|
47
50
|
throw new BoardOpError(`unknown board rpc: ${endpoint}`);
|
|
48
51
|
}
|
|
49
52
|
function requireRevision(value, endpoint) {
|
package/lib/service.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ export declare class BoardModel {
|
|
|
47
47
|
openAttachment(callerSessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
48
48
|
opened: true;
|
|
49
49
|
}>;
|
|
50
|
+
openUrl(url: string): Promise<{
|
|
51
|
+
opened: true;
|
|
52
|
+
}>;
|
|
50
53
|
fireDue(): Promise<BoardReminderEvent[]>;
|
|
51
54
|
private loadUnifiedGlobal;
|
|
52
55
|
private ensureLegacyMigration;
|
|
@@ -69,6 +72,9 @@ export declare class BoardService extends TypertRemoteService {
|
|
|
69
72
|
openAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
70
73
|
opened: true;
|
|
71
74
|
}>;
|
|
75
|
+
openUrl(url: string): Promise<{
|
|
76
|
+
opened: true;
|
|
77
|
+
}>;
|
|
72
78
|
fireDue(): Promise<BoardReminderEvent[]>;
|
|
73
79
|
}
|
|
74
80
|
export declare function createBoardService(ctx: Context, store: BoardStore, options?: BoardServiceOptions): BoardService;
|
package/lib/service.js
CHANGED
|
@@ -131,6 +131,13 @@ export class BoardModel {
|
|
|
131
131
|
await this.openFile(path);
|
|
132
132
|
return { opened: true };
|
|
133
133
|
}
|
|
134
|
+
async openUrl(url) {
|
|
135
|
+
const parsed = new URL(url);
|
|
136
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
137
|
+
throw new BoardOpError('only http and https URLs can be opened');
|
|
138
|
+
await this.openFile(parsed.href);
|
|
139
|
+
return { opened: true };
|
|
140
|
+
}
|
|
134
141
|
async fireDue() {
|
|
135
142
|
return this.enqueueMutation(async () => {
|
|
136
143
|
const events = [];
|
|
@@ -263,6 +270,9 @@ function applySessionMutation(global, sessionId, op, now, createId) {
|
|
|
263
270
|
case 'addLog':
|
|
264
271
|
ownCard(op.cardId);
|
|
265
272
|
break;
|
|
273
|
+
case 'deleteLog':
|
|
274
|
+
ownCard(op.cardId);
|
|
275
|
+
break;
|
|
266
276
|
case 'moveCard':
|
|
267
277
|
ownCard(op.cardId);
|
|
268
278
|
ownColumn(op.columnId);
|
|
@@ -388,6 +398,9 @@ let BoardService = (() => {
|
|
|
388
398
|
openAttachment(sessionId, scope, cardId, attachmentId) {
|
|
389
399
|
return this.model.openAttachment(sessionId, scope, cardId, attachmentId);
|
|
390
400
|
}
|
|
401
|
+
openUrl(url) {
|
|
402
|
+
return this.model.openUrl(url);
|
|
403
|
+
}
|
|
391
404
|
fireDue() {
|
|
392
405
|
return this.model.fireDue();
|
|
393
406
|
}
|
package/lib/tools.js
CHANGED
|
@@ -300,6 +300,15 @@ const MUTATE_OP = {
|
|
|
300
300
|
text: { type: 'string', required: true },
|
|
301
301
|
},
|
|
302
302
|
},
|
|
303
|
+
{
|
|
304
|
+
type: 'object',
|
|
305
|
+
additionalProperties: false,
|
|
306
|
+
properties: {
|
|
307
|
+
type: { type: 'string', const: 'deleteLog', required: true, description: 'Permanently delete one process log and its attachments. Only use on an explicit user request.' },
|
|
308
|
+
cardId: { type: 'string', required: true, description: 'Exact card_* ID returned by board_search or board_get.' },
|
|
309
|
+
logId: { type: 'string', required: true, description: 'Exact log_* ID returned in the card logs from board_get.' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
303
312
|
{
|
|
304
313
|
type: 'object',
|
|
305
314
|
additionalProperties: false,
|
|
@@ -641,7 +650,11 @@ function renderGet(value) {
|
|
|
641
650
|
const note = card.body.length > 0 ? `\nnotes: ${card.body}` : '';
|
|
642
651
|
const reminder = card.reminder ? `\nreminder: ${card.reminder.status} at ${card.reminder.at}` : '';
|
|
643
652
|
const tags = card.tagIds.length > 0 ? `\ntags: ${card.tagIds.join(', ')}` : '';
|
|
644
|
-
|
|
653
|
+
const logs = card.logs.length === 0 ? '' : `\nlogs:\n${card.logs.map((log) => {
|
|
654
|
+
const attachments = log.attachments.length === 0 ? '' : `; attachments: ${log.attachments.map((attachment) => `${attachment.name} (${attachment.id})`).join(', ')}`;
|
|
655
|
+
return `- ${log.id} at ${log.createdAt}: ${log.text}${attachments}`;
|
|
656
|
+
}).join('\n')}`;
|
|
657
|
+
return `card ${card.id}: ${card.title}${note}${tags}${reminder}${logs}`;
|
|
645
658
|
}
|
|
646
659
|
return renderBoard(value.board);
|
|
647
660
|
}
|
|
@@ -682,6 +695,8 @@ function renderApply(op, board) {
|
|
|
682
695
|
const log = board.cards.find((item) => item.id === op.cardId)?.logs.at(-1);
|
|
683
696
|
return `${prefix} Added process log${log === undefined ? '' : ` ${log.id}`} to card ${op.cardId}.`;
|
|
684
697
|
}
|
|
698
|
+
case 'deleteLog':
|
|
699
|
+
return `${prefix} Permanently deleted process log ${op.logId} from card ${op.cardId}.`;
|
|
685
700
|
case 'restoreCards':
|
|
686
701
|
return `${prefix} Restored cards: ${op.cards.map((item) => item.cardId).join(', ')}.`;
|
|
687
702
|
case 'deleteCard':
|