Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Description formats:
| 061 | Feature | [Add generate courses and speeds for track tool spec](docs/ideas/061-generate-courses-speeds.md) (requires #049) | 4 | 3 | 5 | 12 | Low | approved |
| 011 | Documentation | Create Jupyter notebook example demonstrating debrief-calc Python API | 4 | 4 | 4 | 12 | Low | approved |
| 065 | Feature | [Implement Phase 2 tools: track/styling + dataset/export](docs/ideas/065-implement-tools-phase2-styling-export.md) [E01] — 15 styling and export tools (requires #064) | 4 | 3 | 4 | 11 | Medium | approved |
| 076 | Feature | [Implement replay and parameter tuning](docs/ideas/076-replay-tune.md) [E02] — parameter editing, positional replay, revert operations (requires #071, #074) | 5 | 4 | 2 | 11 | High | approved |
| 076 | Feature | [Implement replay and parameter tuning](specs/076-replay-tune/spec.md) [E02] — parameter editing, positional replay, revert operations (requires #071, #074) | 5 | 4 | 2 | 11 | High | implementing |
| 060 | Feature | [Add resample track tool spec](docs/ideas/060-resample-track.md) (requires #049) | 4 | 3 | 4 | 11 | Medium | approved |
| 002 | Feature | Add MCP wrapper for debrief-io service | 4 | 3 | 4 | 11 | Medium | approved |
| 005 | Tech Debt | Add cross-service end-to-end workflow tests (io -> stac -> calc) | 4 | 2 | 5 | 11 | Low | approved |
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Institutional knowledge lives in `docs/project_notes/` for consistency across se
- TypeScript 5.x (Log Service in session-state package, VS Code extension stacService) + Existing Log Service (#071), Snapshot Service (#074), stacService, session-state Zustand store, Node.js `fs/promises`, `crypto.randomUUID()` (075-branching)
- TypeScript 5.x (shared component library) + React 18.x (peer), vscrui ^0.1.0 (icons, existing), memfs ^4.x (devDependency for fixtures) (077-stac-file-tree)
- N/A — reads filesystem via injected adapter, does not persist state (077-stac-file-tree)
- TypeScript 5.x (session-state package, VS Code extension, shared components) + Zustand ^5.0.0 (session-state store), React 18.x (shared components), VS Code Extension API ^1.85.0, existing `@debrief/session-state` (Log Service, Snapshot Service), existing `calcService` (MCP tool invocation), existing `stacService` (file I/O) (076-replay-tune)
- Python 3.11 (debrief-calc service), TypeScript 5.x (VS Code extension, web-shell) + `debrief_calc` registry + `@tool` decorator (Python), `MCPToolDefinition` types (TypeScript). Standard library `math` module for trig functions — no external geo libraries. (056-move-shape)
- N/A — pure transformation tool, no persistence (caller handles STAC writes) (056-move-shape)

Expand Down
241 changes: 221 additions & 20 deletions apps/vscode/src/views/logPanelView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
* Subscribes to SessionManager for active session changes.
* Calls logService.getTimeline() to fetch timeline entries.
* Routes messages between webview and extension.
* Handles Phase 6 replay/tune/revert operations.
*
* Feature: 072-log-panel (E02, Phase 2)
* Updated: 076-replay-tune (E02, Phase 6)
*/

import * as vscode from 'vscode';
import {
type SessionStoreApi,
type LogService,
type LogEntry,
type ReplayResult,
} from '@debrief/session-state';
import type { SessionManager } from '../services/sessionManager';

Expand All @@ -37,6 +40,8 @@ interface TimelineEntry {
executionDuration: string;
generatedResultId: string | null;
operationCategory: OperationCategory;
deleted?: boolean;
tuneAnnotation?: { parameter: string; previousValue: unknown; newValue: unknown } | null;
}

// Webview → Extension messages
Expand All @@ -63,12 +68,42 @@ interface WebviewReadyMessage {
type: 'webviewReady';
}

// Phase 6 messages (Feature: 076-replay-tune)
interface TuneRequestMessage {
type: 'tune:request';
payload: { activityId: string; parameter: string; newValue: unknown };
}

interface RevertToRequestMessage {
type: 'revert-to:request';
payload: { activityId: string };
}

interface RevertThisRequestMessage {
type: 'revert-this:request';
payload: { activityId: string };
}

interface RestoreRequestMessage {
type: 'restore:request';
payload: { activityId: string };
}

interface ReplayCancelMessage {
type: 'replay:cancel';
}

type WebviewMessage =
| EntrySelectMessage
| EntryDeselectMessage
| ActionInvokeMessage
| ModeChangeMessage
| WebviewReadyMessage;
| WebviewReadyMessage
| TuneRequestMessage
| RevertToRequestMessage
| RevertThisRequestMessage
| RestoreRequestMessage
| ReplayCancelMessage;

// Tool category mapping for operation classification
const TOOL_CATEGORY_MAP: Record<string, OperationCategory> = {
Expand Down Expand Up @@ -103,16 +138,17 @@ function toTimelineEntry(entry: LogEntry): TimelineEntry {
executionDuration: entry.executionDuration,
generatedResultId: entry.generatedResultId ?? null,
operationCategory: classifyOperation(entry.wasGeneratedBy.tool),
deleted: entry.deleted === true,
tuneAnnotation: entry.tune !== null
? { parameter: entry.tune.parameter, previousValue: entry.tune.previousValue, newValue: entry.tune.newValue }
: null,
};
}

// Phase/action availability messages
const ACTION_MESSAGES: Record<string, string> = {
tune: 'Parameter tuning is planned for Phase 6.',
revertTo: 'Revert to Here is planned for Phase 4.',
revertThis: 'Revert This is planned for Phase 4.',
snapshot: 'Snapshot creation is planned for Phase 4.',
rationale: 'Rationale annotations are planned for Phase 6.',
// Action availability messages (snapshot and rationale remain stubs)
const STUB_ACTION_MESSAGES: Record<string, string> = {
snapshot: 'Snapshot creation — use the Snapshot Service directly.',
rationale: 'Rationale annotations are planned for a future phase.',
};

export class LogPanelViewProvider implements vscode.WebviewViewProvider {
Expand All @@ -136,6 +172,9 @@ export class LogPanelViewProvider implements vscode.WebviewViewProvider {
// Feature name resolution
private _featureNames: Record<string, string> = {};

// Phase 6: active replay abort controller
private _replayAbortController?: AbortController;

constructor(
extensionUri: vscode.Uri,
private readonly _context: vscode.ExtensionContext,
Expand Down Expand Up @@ -329,19 +368,58 @@ export class LogPanelViewProvider implements vscode.WebviewViewProvider {
break;

case 'action:invoke':
// All actions return "not available" in Phase 2
// Phase 6 actions are wired via dedicated message types.
// action:invoke now only handles stubs (snapshot, rationale).
{
const actionMsg =
ACTION_MESSAGES[message.payload.actionType] ??
'This action is not yet available.';
this._postMessage({
type: 'action:result',
payload: {
actionType: message.payload.actionType,
available: false,
message: actionMsg,
},
});
const actionType = message.payload.actionType;
if (actionType === 'tune' || actionType === 'revertTo' || actionType === 'revertThis') {
// These are handled via dedicated Phase 6 messages from the webview.
// If the webview sends action:invoke for them, it's the old path — inform it.
this._postMessage({
type: 'action:result',
payload: {
actionType,
available: false,
message: 'Use the inline parameter editor or revert buttons.',
},
});
} else {
const actionMsg =
STUB_ACTION_MESSAGES[actionType] ??
'This action is not yet available.';
this._postMessage({
type: 'action:result',
payload: {
actionType,
available: false,
message: actionMsg,
},
});
}
}
break;

// Phase 6: tune/revert/restore operations (Feature: 076-replay-tune)
case 'tune:request':
void this._handleTuneRequest(message.payload);
break;

case 'revert-to:request':
void this._handleRevertToRequest(message.payload);
break;

case 'revert-this:request':
void this._handleRevertThisRequest(message.payload);
break;

case 'restore:request':
void this._handleRestoreRequest(message.payload);
break;

case 'replay:cancel':
if (this._replayAbortController) {
this._replayAbortController.abort();
this._replayAbortController = undefined;
}
break;

Expand All @@ -367,6 +445,129 @@ export class LogPanelViewProvider implements vscode.WebviewViewProvider {
}
}

// ─── Phase 6 handlers (Feature: 076-replay-tune) ──────────────────

private async _handleTuneRequest(payload: {
activityId: string;
parameter: string;
newValue: unknown;
}): Promise<void> {
if (!this._logService || !this._getStorePath || !this._getItemPath) {
return;
}
const storePath = this._getStorePath();
const itemPath = this._getItemPath();
if (!storePath || !itemPath) {
return;
}

try {
const result = await this._logService.tuneEntry(
storePath, itemPath,
payload.activityId, payload.parameter, payload.newValue
);
this._sendReplayResult(result);
await this._sendTimelineUpdate();
} catch (err) {
this._postMessage({
type: 'replay:error',
payload: { message: err instanceof Error ? err.message : String(err) },
});
}
}

private async _handleRevertToRequest(payload: {
activityId: string;
}): Promise<void> {
if (!this._logService || !this._getStorePath || !this._getItemPath) {
return;
}
const storePath = this._getStorePath();
const itemPath = this._getItemPath();
if (!storePath || !itemPath) {
return;
}

try {
await this._logService.revertTo(storePath, itemPath, payload.activityId);
this._postMessage({
type: 'action:result',
payload: {
actionType: 'revertTo',
available: false,
message: 'Reverted successfully. Operations after the selected point have been removed.',
},
});
await this._sendTimelineUpdate();
} catch (err) {
this._postMessage({
type: 'replay:error',
payload: { message: err instanceof Error ? err.message : String(err) },
});
}
}

private async _handleRevertThisRequest(payload: {
activityId: string;
}): Promise<void> {
if (!this._logService || !this._getStorePath || !this._getItemPath) {
return;
}
const storePath = this._getStorePath();
const itemPath = this._getItemPath();
if (!storePath || !itemPath) {
return;
}

try {
const result = await this._logService.revertThis(
storePath, itemPath, payload.activityId
);
this._sendReplayResult(result);
await this._sendTimelineUpdate();
} catch (err) {
this._postMessage({
type: 'replay:error',
payload: { message: err instanceof Error ? err.message : String(err) },
});
}
}

private async _handleRestoreRequest(payload: {
activityId: string;
}): Promise<void> {
if (!this._logService || !this._getStorePath || !this._getItemPath) {
return;
}
const storePath = this._getStorePath();
const itemPath = this._getItemPath();
if (!storePath || !itemPath) {
return;
}

try {
const result = await this._logService.restoreEntry(
storePath, itemPath, payload.activityId
);
this._sendReplayResult(result);
await this._sendTimelineUpdate();
} catch (err) {
this._postMessage({
type: 'replay:error',
payload: { message: err instanceof Error ? err.message : String(err) },
});
}
}

private _sendReplayResult(result: ReplayResult): void {
this._postMessage({
type: 'replay:result',
payload: result as unknown as Record<string, unknown>,
});
}

// ─── End Phase 6 handlers ────────────────────────────────────────

/**
* Dispose resources.
*/
Expand Down
Loading
Loading