Task work log, inline writing assistant, task editor sidebar layout

Backend:
- Migration 0021: task_logs table (FK → notes + users, CASCADE, indexed)
- models/task_log.py: SQLAlchemy model with to_dict()
- services/task_logs.py: CRUD with ownership checks, _UNSET sentinel for optional duration clear
- routes/task_logs.py: GET/POST/PATCH/DELETE /api/tasks/<id>/logs
- services/tools.py: log_work LLM tool (resolves task by title, creates log entry)
- services/generation_task.py: retry assist generation up to 3× on HTTP 500

Frontend:
- types/task.ts: TaskLog interface
- TaskLogSection.vue: chronological work log with date+time timestamps, duration badge, inline edit, autofocus
- InlineAssistPanel.vue: streaming preview + diff review rendered inline in editor column
- useAssist.ts: removed chatStore.chatReady gate; toast notifications for errors
- NoteEditorView.vue + TaskEditorView.vue: inline assist panel, aside restricted to idle state
- TaskEditorView.vue: two-column layout (editor+log left, metadata sidebar right), body defaults to Preview, sidebarOpen accordion for mobile
- editor-shared.css: .assist-active-hint style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 13:05:26 -05:00
parent dc39a56293
commit 9bf047ec45
16 changed files with 1309 additions and 339 deletions
+5 -3
View File
@@ -1,6 +1,6 @@
import { ref, computed, watch, type Ref } from "vue";
import { apiPost, apiSSEStream, type SSEStreamHandle } from "@/api/client";
import { useChatStore } from "@/stores/chat";
import { useToastStore } from "@/stores/toast";
import {
parseMarkdownSections,
type MarkdownSection,
@@ -42,7 +42,7 @@ function computeDiff(a: string, b: string): DiffLine[] {
}
export function useAssist(body: Ref<string>) {
const chatStore = useChatStore();
const toast = useToastStore();
const state = ref<AssistState>("idle");
const sections = ref<MarkdownSection[]>([]);
@@ -80,7 +80,6 @@ export function useAssist(body: Ref<string>) {
() =>
target.value !== null &&
instruction.value.trim().length > 0 &&
chatStore.chatReady &&
state.value !== "streaming"
);
@@ -154,6 +153,7 @@ export function useAssist(body: Ref<string>) {
}
if (evt.event === "error") {
error.value = evt.data.error as string;
toast.show("Assist failed: " + error.value, "error");
state.value = "idle";
streamHandle = null;
}
@@ -173,6 +173,7 @@ export function useAssist(body: Ref<string>) {
}
} catch (e) {
error.value = e instanceof Error ? e.message : "Request failed";
toast.show("Assist failed: " + error.value, "error");
state.value = "idle";
streamHandle = null;
}
@@ -202,6 +203,7 @@ export function useAssist(body: Ref<string>) {
const currentSlice = body.value.slice(t.startOffset, t.endOffset);
if (currentSlice !== t.text) {
error.value = "The document changed since this suggestion was made. Please clear and try again.";
toast.show(error.value, "error");
state.value = "idle";
return body.value;
}