ガントで範囲外にはみ出すタスクの全体時間と往復数も範囲内に揃える

表示範囲を絞ったとき、作業時間だけが範囲内に切り詰められ、全体時間・往復数・
ツール呼び出し数はセッション全体の値のままだったので、1日表示に「全体 793時間」
のような数字が並んで読み違えやすかった。

範囲の左端で切れている行は全体時間を「表示範囲の開始→セッション終了」にし、
往復数とツール数も範囲内のターンだけを数える。切り詰めた行には clipped を立て、
ツールチップのラベルを「全体」→「表示範囲内」に変える。開始→終了の日時は
実際の値のままなので、範囲外へ続いていることはそこで分かる。

ガントの CSV にも clipped 列を追加した。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 11:17:06 +09:00
co-authored by Claude Opus 5
parent afae15b30c
commit d23bcabd76
4 changed files with 28 additions and 6 deletions
+2
View File
@@ -68,3 +68,5 @@ python chktime.py --port 8799 --no-browser
- 自動追従(SSE)で作り直すときも、選択・スクロール位置・絞り込み・タブは保つ(`refreshFollow()`)。
- ガントの軸・背景・枠組みは `timeScale` / `axisSvg` / `gridSvg` / `ganttFrame` をタスク単位と
ターン単位で共有している。
- `/api/timeline` は表示範囲より前から続くセッションの全体時間・往復数・ツール数も範囲内に切り詰め、
`clipped` を立てて返す(作業時間だけ切り詰めると数字が食い違うため)。
+2
View File
@@ -77,6 +77,7 @@ python chktime.py
- 表示範囲は **1日 / 3日 / 7日 / 14日 / 30日 / 全期間**。1〜3日なら軸が時刻目盛りに切り替わります。土日の帯・日境界の線・現在時刻の点線付き。
- 横断モードでは行の色がプロジェクトを表します(凡例は作業時間上位 7 件)。
- 行にカーソルを当てるとタイトル・プロジェクト・開始→終了・全体/作業/往復数、クリックするとそのタスクの「やりとり」を開きます。
- **表示範囲より前から続いているタスクは、全体時間・往復数も範囲内だけを数えます**(ツールチップの表示が「全体」→「表示範囲内」に変わります)。上の「最長タスク」も同じ基準です。開始→終了の日時だけは実際の値を出すので、範囲外にはみ出していることはそこで分かります。
- 同じ時間帯に複数のバーが重なっていれば、セッションを並行して動かしていた時間です。
#### ターン単位
@@ -90,6 +91,7 @@ python chktime.py
`CSV` ボタンでタスク一覧・ターン一覧・日別集計・ガントのタスク一覧をそれぞれ書き出せます(Excel 用 BOM 付き UTF-8)。
横断モードのタスク一覧 CSV にはプロジェクト列が付きます。日別集計 CSV はプロジェクトごとの列を持つ横持ち形式です。
ガントの CSV には `clipped` 列があり、`1` なら表示範囲で切り詰めた行です。
## 自動追従
+22 -5
View File
@@ -606,6 +606,9 @@ class Store:
"""ガント表示用。セッションを1本のバーとして、内側に応答中の区間を返す。
spans は各ターンの [発言時刻, 応答完了] を、隙間 1 秒以内なら繋げたもの。
表示範囲より前から続いているセッションは、全体時間・往復数・ツール呼び出しも
範囲内に切り詰めて返す(作業時間だけ切り詰めると数字が食い違うため)。
clipped が真なら切り詰めたもの。
"""
paths = {p["id"]: p["path"] for p in self.projects()}
if project_id:
@@ -633,6 +636,8 @@ class Store:
continue
spans = []
active = 0
turn_count = 0
tool_calls = 0
for turn in session["turns"]:
a = turn["ts"]
if a is None:
@@ -641,10 +646,20 @@ class Store:
if t0 is not None and b < t0:
continue
active += turn["durMs"] or 0
turn_count += 1
tool_calls += turn["toolCalls"]
if spans and a - spans[-1][1] <= 1:
spans[-1][1] = max(spans[-1][1], b)
else:
spans.append([a, b])
clipped = t0 is not None and start < t0
if clipped:
# バーが左端で切れているぶん、全体時間も見えている範囲だけにする
wall = ms(end if end is not None else start, t0)
else:
wall = session["wallMs"]
turn_count = session["turnCount"]
tool_calls = session["toolCalls"]
rows.append(
{
"id": session["id"],
@@ -655,10 +670,11 @@ class Store:
"end": session["end"],
"startEpoch": round(start, 1),
"endEpoch": round(end if end is not None else start, 1),
"wallMs": session["wallMs"],
"wallMs": wall,
"activeMs": active,
"turnCount": session["turnCount"],
"toolCalls": session["toolCalls"],
"turnCount": turn_count,
"toolCalls": tool_calls,
"clipped": clipped,
"spans": [[round(a, 1), round(b, 1)] for a, b in spans],
}
)
@@ -862,11 +878,12 @@ def turns_csv(session):
def timeline_csv(data):
buf = io.StringIO(newline="")
writer = csv.writer(buf)
writer.writerow(["project", "session_id", "title", "start", "end", "wall", "active", "turns", "tool_calls"])
writer.writerow(["project", "session_id", "title", "start", "end", "wall", "active", "turns", "tool_calls", "clipped"])
for s in data["sessions"]:
writer.writerow(
[s["projectPath"], s["id"], s["title"], s["start"] or "", s["end"] or "",
fmt_dur(s["wallMs"]), fmt_dur(s["activeMs"]), s["turnCount"], s["toolCalls"]]
fmt_dur(s["wallMs"]), fmt_dur(s["activeMs"]), s["turnCount"], s["toolCalls"],
"1" if s["clipped"] else ""]
)
return buf.getvalue()
+2 -1
View File
@@ -825,7 +825,8 @@ function renderGanttTasks() {
s.title,
cross ? s.projectPath : '',
`${fmtDateTime(s.start)} → ${fmtDateTime(s.end)}`,
`全体 ${fmtDur(s.wallMs)} · 作業 ${fmtDur(s.activeMs)} · ${s.turnCount} 往復`,
// 表示範囲より前から続いているタスクは、数字が範囲内だけのものだと分かるようにする
`${s.clipped ? '表示範囲内' : '全体'} ${fmtDur(s.wallMs)} · 作業 ${fmtDur(s.activeMs)} · ${s.turnCount} 往復`,
]
.filter(Boolean)
.join('\n');