diff --git a/CLAUDE.md b/CLAUDE.md index 65b188e..c273036 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,3 +68,5 @@ python chktime.py --port 8799 --no-browser - 自動追従(SSE)で作り直すときも、選択・スクロール位置・絞り込み・タブは保つ(`refreshFollow()`)。 - ガントの軸・背景・枠組みは `timeScale` / `axisSvg` / `gridSvg` / `ganttFrame` をタスク単位と ターン単位で共有している。 +- `/api/timeline` は表示範囲より前から続くセッションの全体時間・往復数・ツール数も範囲内に切り詰め、 + `clipped` を立てて返す(作業時間だけ切り詰めると数字が食い違うため)。 diff --git a/README.md b/README.md index cb55e75..62d95ad 100644 --- a/README.md +++ b/README.md @@ -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` なら表示範囲で切り詰めた行です。 ## 自動追従 diff --git a/chktime.py b/chktime.py index 81e90b7..85cd51a 100644 --- a/chktime.py +++ b/chktime.py @@ -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() diff --git a/web/app.js b/web/app.js index e446ff7..ab3f20b 100644 --- a/web/app.js +++ b/web/app.js @@ -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');