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
48 changes: 48 additions & 0 deletions .github/workflows/wiki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Wiki

# Publishes wiki/ to the repository's GitHub wiki whenever it changes on main,
# so the wiki's pages are reviewed in pull requests like everything else.
# Pages deleted from wiki/ are deleted from the wiki too; edits made in the
# GitHub wiki UI are overwritten by the next publish.

on:
push:
branches: [main]
paths: ['wiki/**', '.github/workflows/wiki.yml']
workflow_dispatch:

permissions:
contents: write

concurrency:
group: wiki
cancel-in-progress: false

jobs:
publish:
name: Publish wiki
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Clone the wiki
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: git clone --depth 1 "https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.wiki.git" "$RUNNER_TEMP/wiki"

- name: Copy the pages
run: |
find "$RUNNER_TEMP/wiki" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
cp -R wiki/. "$RUNNER_TEMP/wiki/"

- name: Commit and push
working-directory: ${{ runner.temp }}/wiki
run: |
git add -A
if git diff --cached --quiet; then
echo "The wiki is up to date."
exit 0
fi
git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
commit -m "Publish wiki from ${{ github.repository }}@${GITHUB_SHA::7}"
git push origin HEAD
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,4 @@ Code map:
| `docs/TIMESHEETS.md` | Timesheets: rounding, trims, destinations, storage |
| `docs/TIME-EXTENSION.md` | How time tracking became the devlog-time extension |
| `docs/BLOCK-PAGES.md` | Block pages and todos as blocks |
| `wiki/` | The GitHub wiki's pages: user guide, extension developer docs and API reference (see `wiki/Development.md` to publish) |
104 changes: 104 additions & 0 deletions wiki/API-Versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# API versions

The extension API is versioned separately from the app
(`EXTENSION_API_VERSION` in `packages/core/src/extensions.ts`, `API_VERSION`
in `@devlog/extension-api`). Every change so far has been **additive**: there
has been no 2.0, and an extension built for 1.0 still loads in an app that
provides 1.7.

- Declare the **lowest** version you need in the manifest: `"api": "^1.6.0"`.
- `ctx.apiVersion` tells you what the running app provides, for optional
features.

| API | Devlog | Headline |
|---|---|---|
| 1.7 | 0.18 | Activity and ranges, managed canvases, sending through destinations, `openPage`, non-inherited fields |
| 1.6 | 0.17 | Node types, canvas writes, command context/keys/menus/post, events, pick/open/highlight, tray, time providers, canvas-header views |
| 1.5 | 0.16 | Views and `@devlog/ui` |
| 1.4 | — | Todos; blocks inside blocks |
| 1.3 | — | Timesheet destinations |
| 1.2 | — | Unrestricted (trusted) extensions, `packageDir` |
| 1.1 | — | `machine`, focus providers |
| 1.0 | — | The base API |

## 1.7

- `devlog.activity(from, to)`: what the app recorded, merged with providers.
- `devlog.range(from, to)`: day files with blocks written in a range.
- `devlog.managedCanvas(key, { title })`: a canvas the extension keeps;
`addBlock` gains `kind` and free `date` there, and `editBlock` works on any
block there.
- `ui.openPage(viewId)`.
- `permissions.send`; `destinations.list()`, `preview(to, sheet)`,
`send(to, sheet)`; types `DestinationInfo`, `SheetToSend`.
- Canvas fields may say `"inherited": false`.
- Views send `key` messages for shortcuts they did not use.
- Types: `ActivityRecord`, `ExtensionDayBlocks`.

## 1.6

- **Node types**: `contributes.nodeTypes`; `ExtensionCanvas.type`.
- `devlog.createCanvas`, `updateCanvas`, `editBlock`, `promote`.
- `devlog.onBlockAdded(cb)`.
- Commands: a `CommandContext` argument (`source`, `canvasId`, `date`,
`blockId`); manifest `keybinding`, `menus` (`canvas`, `block`, `tray`),
`nodeType`, `post`, `tag`.
- `ui.pick`, `ui.open`, `ui.highlight`.
- `app.setTrayLabel`, `app.keepRunning`.
- `activity.idleAfter(minutes)`.
- `provide.activity(fn)` and `TimeEvent`.
- Views: `canvasHeader` placement, `nodeType` on views, the `context`
message and `ViewContext`.

## 1.5

- `contributes.views` with placements `page`, `statusbar`, `popover`.
- `views.handle`, `views.post`.
- The view message protocol (`@devlog/extension-api/view`) and `@devlog/ui`.

## 1.4

- `devlog.todos({ doneSince })` and `ExtensionTodo`.
- `addBlock` options `parentId` + `date` (inside a block) and `todo`.

## 1.3

- `contributes.destinations`, `destinations.register(id, { preview, send })`.
- Types `DestinationSheet`, `DestinationEntry`, `DestinationLine`,
`SendResult`, `Destination`.

## 1.2

- `permissions.unrestricted` and `ctx.packageDir`.

## 1.1

- `ctx.machine`.
- `provide.focus(fn)` and `FocusEvent`.

## 1.0

- `ctx.id`, `ctx.apiVersion`.
- `devlog.canvases`, `field`, `days`, `blocks`, `search`, `addBlock`.
- `settings.get` / `onChange`; `secrets.get` / `set` / `delete`.
- `files.repo` / `files.local`.
- `activity.on`.
- `ui.notify`, `ui.confirm`.
- `commands.register`.
- Manifest: `canvasFields`, `settings`, `secrets`, `commands`, `check`,
`permissions.read` / `write` / `network`, `appendOnly`.

## Changing the API (for Devlog contributors)

1. Add the member to `packages/extension-api/src/index.ts` with a comment
naming the new version, and to `APP_METHODS` in `protocol.ts` if the
extension calls the app.
2. Implement it in the host process (`src/main/extensions/hostProcess.ts`)
and the manager (`src/main/extensions/manager.ts`), filtered by the grant.
3. Mirror it in the test harness (`packages/extension-api/src/testing.ts`).
4. Bump `API_VERSION` and `EXTENSION_API_VERSION` together, and the version
in `docs/EXTENSIONS.md` and `packages/extension-api/README.md`.
5. Cover it in `tests/extensions.test.ts` through the real host (the
`probe` or `shaper` fixture).
6. Update this wiki: the [API reference](Extension-API-Reference), the
[types](Extension-Types) and this page.
140 changes: 140 additions & 0 deletions wiki/Activity-and-Time-Data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Activity and time data

Devlog keeps no clock of its own. The machine's state (locked, idle, asleep)
is the app's; **time** and **window focus** come from extensions. This page
covers the four pieces of API around that:

| API | Direction | Since |
|---|---|---|
| `ctx.activity.on(cb)` | app → extension: pause and resume notices | 1.0 |
| `ctx.activity.idleAfter(minutes)` | extension → app: when to count the machine idle | 1.6 |
| `ctx.provide.activity(fn)` | extension → app views: time-tracking events | 1.6 |
| `ctx.provide.focus(fn)` | extension → app views: window focus events | 1.1 |
| `ctx.devlog.activity(from, to)` | app → extension: everything recorded, merged | 1.7 |

The design is in
[`docs/DESIGN.md` → Time](https://github.com/sdeken/devlog/blob/main/docs/DESIGN.md#time-devlog-time-and-the-activity-log)
and [`docs/TIME-EXTENSION.md`](https://github.com/sdeken/devlog/blob/main/docs/TIME-EXTENSION.md).

## Machine state: pause and resume

```js
ctx.activity.on((n) => {
if (n.type === 'pause') clock.pause(n.reason, n.t) // 'locked' | 'idle' | 'suspended'
if (n.type === 'resume') clock.resume(n.t)
})
```

- The app watches Electron's `powerMonitor` (lock/unlock, suspend/resume)
and polls the system idle state every 15 seconds.
- Pauses are tracked **per reason**: waking from sleep clears only
"suspended", input after idle clears only "idle", and unlocking clears
everything. `resume` is sent when no reason is left.
- **Idle detection is opt-in.** Call `ctx.activity.idleAfter(minutes)`; the
app uses the shortest time any extension asks for (0 means never; at most
1440). Without any request, idleness never pauses.

```js
const idle = () => Number(ctx.settings.get('idle_minutes') ?? '10')
ctx.activity.idleAfter(idle())
ctx.settings.onChange(() => ctx.activity.idleAfter(idle()))
```

## Providing time: `provide.activity`

An extension that tracks time gives the app its events, and the app's
Timeline, Weekly review, Summary and Timesheet are drawn from them.

```js
ctx.provide.activity(async (fromDate, toDate) => {
// Every machine's events between two local dates, inclusive.
const out = []
for (const date of datesBetween(fromDate, toDate)) {
for (const f of await ctx.files.repo.list()) {
if (!f.path.endsWith(`${date}.jsonl`)) continue
const text = (await ctx.files.repo.readText(f.path)) ?? ''
for (const line of text.split('\n')) if (line) out.push(JSON.parse(line))
}
}
return out // TimeEvent[]
})
```

`TimeEvent`:

| `type` | Fields | Meaning |
|---|---|---|
| `start` | `canvasId?` | The clock started with the app (and the task it restored). |
| `task` | `canvasId` (or `null`), `blockId?` | The active canvas changed; `null` stops it. |
| `stop` | — | The clock stopped with the app. |
| `heartbeat` | — | Still running. |

Every event carries `t` (ISO) and `machine` (`ctx.machine` where it was
recorded). Events of other types are ignored; so are events without a valid
`t`.

How the app uses them:

- It **replays** your events together with its own record of locks, idle and
sleep into task segments (task → next task, stop or pause; resumed on
unpause), per machine, then flattens overlaps between machines (the later
start wins), so time is never counted twice.
- **Heartbeats are the liveness signal.** A gap of more than two heartbeats
is read as "the app was not running", so a crash cannot book a weekend.
devlog-time writes one every five minutes, and none while paused.
- While **any** extension provides time, the app also writes lock/idle/sleep
and git events to the devlog's `activity/<machine>/…` log. Without one, it
records nothing about time.
- Explicit durations (`[2h]` on a block) and user corrections (`exclude`
events) are applied on top.

Keep per-machine logs in per-machine files
(`ctx.files.repo`, `${ctx.machine}/YYYY/MM/YYYY-MM-DD.jsonl`) and declare
them `appendOnly`; machines then never conflict in git.

Also relevant for a time tracker:

- `ctx.ui.highlight(canvasId)`: mark the running task in the sidebar;
commits from a linked repository then land on it when it sits inside the
linked canvas.
- `ctx.app.keepRunning(true)`: time accrues only while Devlog runs, so close
to the tray instead of quitting.
- `ctx.app.setTrayLabel(label)`: show the running task in the tray.
- `ctx.devlog.onBlockAdded(cb)`: devlog-time starts a task when the user
posts on it.

## Providing focus: `provide.focus`

```js
ctx.provide.focus(async (fromDate, toDate) => readFocusLog(fromDate, toDate)) // FocusEvent[]
```

`FocusEvent` is `{ t, app, title, machine }`: the window that came to the
front. The app cleans the stream for its views (drops shell windows such as
the task switcher, folds flips shorter than a few seconds, merges repeats)
but you should record it raw. `app` is cut to 200 characters and `title` to
1000.

Reading the foreground window needs a platform helper, which the sandbox
forbids, so a focus provider normally runs **unrestricted**. devlog-focus is
the complete example (see [Built-in Extensions](Built-in-Extensions)).

## Reading it back: `devlog.activity` (1.7)

```js
const records = await ctx.devlog.activity('2026-09-21', '2026-09-27')
const tracked = records.filter((r) => r.type === 'task')
```

Returns the app's own log merged with what providers return (time and
focus), as `ActivityRecord`s. Filtered by the grant:

- needs a read grant (`No read access` otherwise);
- events on canvases it may not read show `canvasId: null` (git events on
them are dropped);
- `focus` events only with read access to the whole devlog;
- at most 400 days per call.

Providers have 20 seconds to answer and at most 500 000 events per call are
used. A provider that throws is logged and skipped; the views show what the
others gave.
Loading
Loading