@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 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 0 .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; }
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:auto; }
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:absolute; z-index:75; 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); }
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
- return { board, loading, error, conflict, mutate, addLog, downloadAttachment, openAttachment, reload };
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)("span", { className: "board-detail-heading", children: props.t("cardDetails") }),
1636
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
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
- log.text.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: log.text }),
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
@@ -145,6 +145,10 @@ export type MutateOp = {
145
145
  cardId: string;
146
146
  text: string;
147
147
  attachments?: LogAttachment[];
148
+ } | {
149
+ type: 'deleteLog';
150
+ cardId: string;
151
+ logId: string;
148
152
  } | {
149
153
  type: 'moveCard';
150
154
  cardId: string;
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 an immutable process note/blocker/decision; moveCard/reorderColumns organize; archiveCard completes; restoreCard/restoreCards recover. updateCard saves title, notes, startAt/endAt, and timeReminder together.',
10
- '7. Completing means archiveCard, never deleteCard. Delete operations 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).',
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
- return `card ${card.id}: ${card.title}${note}${tags}${reminder}`;
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':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@listen1954/board",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Board for DeepSeek Harness: global and session scopes, natural-language tools, and a native Web UI.",
5
5
  "author": "listen",
6
6
  "type": "module",